textmachine/platform/internal/runs/order_test.go

829 lines
40 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 runs
import (
"errors"
"slices"
"strings"
"testing"
"time"
"textmachine/platform/internal/money"
"textmachine/platform/internal/pgstore"
"textmachine/platform/internal/pricing"
"textmachine/platform/internal/runner"
)
// argvOf is the argv of the LAST unit this fixture started.
func argvOf(t *testing.T, f *fixture) []string {
t.Helper()
starts := f.runner.starts()
if len(starts) == 0 {
t.Fatal("no unit was started")
}
return starts[len(starts)-1].Args
}
// flagValue reads the value of a `--flag value` pair, or "" when the flag is absent.
func flagValue(args []string, flag string) string {
if i := slices.Index(args, flag); i >= 0 && i+1 < len(args) {
return args[i+1]
}
return ""
}
// ⛔ THE ORDER REACHES THE ENGINE, and until this pack it did not. The platform sold CHAPTERS and
// handed the engine a DOLLAR bound alone — so «buy ten chapters» arrived as a sum, and a sum buys
// whatever it buys: measured on real ledgers, ten chapters' worth of money bought sixteen to
// twenty-six (D39.165 §1). The knob said chapters and meant money.
//
// `--max-units` is the same quantity on both sides of the seam: the manifest's unit count is where
// this platform's per-chapter figure comes from, so the conversion is exact rather than estimated.
//
// Mutation caught: dropping maxUnits from TranslateArgs, or from spec.
func TestAPartialOrderReachesTheEngineAsAVolumeAndNotOnlyAsMoney(t *testing.T) {
f := newFixture(t, "10", 500)
run, err := f.svc.Start(f.ctx, StartRequest{UserID: "u1", BookID: f.bookID(t), Chapters: order(7)})
if err != nil {
t.Fatal(err)
}
if err := f.svc.Spawn(f.ctx, run.ID); err != nil {
t.Fatal(err)
}
args := argvOf(t, f)
// The fixture ships one unit per chapter, so seven chapters are seven units.
if got := flagValue(args, "--max-units"); got != "7" {
t.Fatalf("the engine was given --max-units %q, want 7: %q", got, strings.Join(args, " "))
}
// And the MONEY bound is still there beside it: the two are orthogonal, one caps this run's work
// and the other the book's cumulative spend.
if !slices.Contains(args, "--ceiling-usd") {
t.Errorf("the volume bound replaced the money bound: %q", strings.Join(args, " "))
}
}
// The WHOLE-BOOK order carries no volume bound at all, and zero is the flag's own word for
// «unbounded» — so it must not be passed as a number either. A `--max-units 0` would be an order for
// nothing.
func TestAWholeBookOrderCarriesNoVolumeBound(t *testing.T) {
f := newFixture(t, "10", 5)
run, err := f.svc.Start(f.ctx, StartRequest{UserID: "u1", BookID: f.bookID(t)})
if err != nil {
t.Fatal(err)
}
if err := f.svc.Spawn(f.ctx, run.ID); err != nil {
t.Fatal(err)
}
if args := argvOf(t, f); slices.Contains(args, "--max-units") {
t.Errorf("a whole-book order was bounded by volume: %q", strings.Join(args, " "))
}
// …and the book row says so in the only way that survives a re-cut: no boundary at all.
var chapterID, unitID *string
if err := f.store.Pool().QueryRow(f.ctx,
`select ordered_through_chapter_id, ordered_through_unit_id from books where id = $1`,
f.bookID(t)).Scan(&chapterID, &unitID); err != nil {
t.Fatal(err)
}
if chapterID != nil || unitID != nil {
t.Errorf("the whole book froze a boundary: chapter %v unit %v", chapterID, unitID)
}
}
// The allowance is what is LEFT of the order, recomputed at every spawn. Frozen at admission, a run
// respawned after a restart would be handed its whole order a second time and the promise «you
// bought N units» would stop being about N.
//
// Mutation caught: maxUnitsFor ignoring SpawnOrder.Delivered.
func TestTheVolumeAllowanceIsWhatIsLeftOfTheOrderAtEverySpawn(t *testing.T) {
f := newFixture(t, "10", 500)
run, err := f.svc.Start(f.ctx, StartRequest{UserID: "u1", BookID: f.bookID(t), Chapters: order(10)})
if err != nil {
t.Fatal(err)
}
if err := f.svc.Spawn(f.ctx, run.ID); err != nil {
t.Fatal(err)
}
if got := flagValue(argvOf(t, f), "--max-units"); got != "10" {
t.Fatalf("the first spawn was given %q, want 10", got)
}
// Four chapters of the order come back delivered, and the machine reboots.
//
// ⚠ Written as RESOLUTION ROWS and not as chapter counters, because the rows are what the
// allowance is computed from and the counters are derived from them: a fixture that moved only
// the counter would describe a state the materializer cannot produce, and would pass while the
// arithmetic read something else.
if _, err := f.store.Pool().Exec(f.ctx, `
insert into unit_resolutions (book_id, chapter, unit, wave, shipped, flagged, at)
select $1, g, 0, 'edit', true, false, now() from generate_series(1, 4) g`, f.bookID(t)); err != nil {
t.Fatal(err)
}
f.engine.set(statusSpending(money.MicroUSD(100_000)), nil)
f.runner.alive = false
f.svc.Now = func() time.Time { return f.now.Add(2 * time.Hour) }
if err := f.svc.Sweep(f.ctx); err != nil {
t.Fatal(err)
}
got := flagValue(argvOf(t, f), "--max-units")
if got != "6" {
t.Fatalf("the respawn was given --max-units %q, want the six units of the order still owed", got)
}
}
// ⛔ AND IT NEVER FALLS TO ZERO BY ARITHMETIC, because zero is the flag's word for «no bound». A run
// respawned after its order was fully delivered would otherwise be handed the whole book.
func TestAFullyDeliveredOrderIsRespawnedWithOneUnitAndNotWithNoBoundAtAll(t *testing.T) {
l := pgstore.LiveRun{RunID: "run_1", OrderedChapters: 10}
o := pgstore.SpawnOrder{Resolved: true, Units: 10, Delivered: 10,
Order: pgstore.BookOrder{ThroughChapterID: "c10"}}
n, err := maxUnitsFor(l, o)
if err != nil {
t.Fatal(err)
}
if n != 1 {
t.Fatalf("a fully delivered order was given --max-units %d; zero means UNBOUNDED", n)
}
o.Delivered = 40 // more delivered than ordered: free and carried units ride outside the grant
if n, err := maxUnitsFor(l, o); err != nil || n != 1 {
t.Fatalf("an over-delivered order was given %d (%v)", n, err)
}
}
// ⛔ A BOOK CUT AGAIN UNDER A PURCHASE IS A REFUSAL, not a substituted number. The order's boundary is
// stored as an IDENTITY so that a re-cut makes it stop resolving; reading the dangling reference as
// «the whole book» would sell MORE than was bought and as «nothing» would sell less. Both are the
// silent change of a paid order that the identity form exists to prevent.
func TestAnOrderThatNoLongerNamesABoundaryRefusesToSpawn(t *testing.T) {
l := pgstore.LiveRun{RunID: "run_1", OrderedChapters: 10}
dangling := pgstore.SpawnOrder{Resolved: false, Units: 0,
Order: pgstore.BookOrder{ThroughChapterID: "a chapter this cut does not have"}}
if _, err := maxUnitsFor(l, dangling); !errors.Is(err, ErrOrderUnresolvable) {
t.Fatalf("an unresolvable order answered %v, want ErrOrderUnresolvable", err)
}
// The whole-book order has no boundary to lose and is unaffected by any re-cut, which is the
// other half of why it stores none. On a FIRST run it takes no volume bound at all.
if n, err := maxUnitsFor(l, pgstore.SpawnOrder{Resolved: true}); err != nil || n != 0 {
t.Fatalf("a whole-book order answered %d (%v)", n, err)
}
// ⛔ AND THE RE-PASS, which is the one shape that buys no volume: the book's own order says
// nothing about it, and bounding it by that order would leave a re-pass the buyer paid a whole
// book's projection for re-making a single unit.
repass := pgstore.LiveRun{RunID: "run_1", OrderedChapters: 0}
if n, err := maxUnitsFor(repass, pgstore.SpawnOrder{Resolved: true, Units: 4, Delivered: 4,
Order: pgstore.BookOrder{ThroughChapterID: "c2"}}); err != nil || n != 0 {
t.Fatalf("a re-pass was bounded by the book's order: %d (%v)", n, err)
}
}
// ⛔ PD-422: `--resnapshot` RIDES EVERY CONTINUATION, not only a run over an edited bank.
//
// The auto-bank grows by MINING during an ordinary run; mining moves the ENRICHED memory version;
// the enriched version is folded into the EDIT wave's snapshot alone. So the SECOND purchase of a
// mining book meets its own already-pinned edit jobs under a moved snapshot, the drift guard stops
// the engine with exit 1 — which this platform can only report as `failed` — and it does so AFTER
// the hold was taken, with nothing translated. The condition used to be the correction door's flag
// alone, which no amount of mining ever sets.
//
// Mutation caught: `resnapshot := book.BankMoved` — the condition as it shipped.
func TestASecondPurchaseCarriesResnapshotEvenWithoutABankCorrection(t *testing.T) {
f := newFixture(t, "10", 500)
first, err := f.svc.Start(f.ctx, StartRequest{UserID: "u1", BookID: f.bookID(t), Chapters: order(3)})
if err != nil {
t.Fatal(err)
}
if err := f.svc.Spawn(f.ctx, first.ID); err != nil {
t.Fatal(err)
}
// The FIRST purchase carries none: there is nothing pinned yet to be re-pinned, and a flag that
// rode every run would be a consent nobody needed.
if args := argvOf(t, f); slices.Contains(args, "--resnapshot") {
t.Fatalf("the first purchase of a book carries --resnapshot: %q", strings.Join(args, " "))
}
// It ends cleanly. NO bank correction happens — this is the ordinary path, which is the whole
// point: mining moved the bank and nothing recorded that fact.
live := f.live(t)
if err := runner.WriteMarker(f.svc.markerPath(live.RunID, live.AttemptNo),
runner.Marker{Unit: live.UnitName, Result: "exit-code", Code: "exited", Status: "0"}); err != nil {
t.Fatal(err)
}
if err := f.svc.Sweep(f.ctx); err != nil {
t.Fatal(err)
}
second, err := f.svc.Start(f.ctx, StartRequest{UserID: "u1", BookID: f.bookID(t), Chapters: order(3)})
if err != nil {
t.Fatal(err)
}
if err := f.svc.Spawn(f.ctx, second.ID); err != nil {
t.Fatal(err)
}
args := argvOf(t, f)
if !slices.Contains(args, "--resnapshot") {
t.Fatalf("the second purchase of a book carries no --resnapshot, so the drift guard kills it "+
"after the hold (PD-422): %q", strings.Join(args, " "))
}
// …and the consent that rides with it is FUNDED — the run's own hold, never the blanket form: a
// projection grown past what the buyer saw must refuse, not be bought silently.
if want := "--accept-rebill=" + fixtureHold(3).USD(); !slices.Contains(args, want) {
t.Errorf("the continuation's consent is not its own hold (%s): %q", want, strings.Join(args, " "))
}
}
// ⛔ A BOOK THE ENGINE HAS NOT PRICED IS NOT SOLD. This is the line the per-chapter constant used to
// stand on: $0.03 was measured 4.47× low and made the last chapters of every book unbuyable at any
// balance, so replacing it with a quieter guess would keep the shape of the defect. There is one
// source for what a book costs, and when it has not spoken the platform says so.
//
// Mutation caught: any fallback in Order or Start when Priced is false.
func TestAnUnpricedBookIsRefusedRatherThanSoldAtAGuess(t *testing.T) {
f := newFixture(t, "10", 5)
if _, err := f.store.Pool().Exec(f.ctx, `
update books set expected_micro_usd = null, book_once_micro_usd = null, step_max_micro_usd = null
where id = $1`, f.bookID(t)); err != nil {
t.Fatal(err)
}
if _, err := f.svc.Order(f.ctx, "u1", f.bookID(t)); !errors.Is(err, ErrNotPriced) {
t.Errorf("the order form over an unpriced book answered %v, want ErrNotPriced", err)
}
if _, err := f.svc.Start(f.ctx, StartRequest{UserID: "u1", BookID: f.bookID(t), Chapters: order(2)}); !errors.Is(err, ErrNotPriced) {
t.Errorf("a purchase of an unpriced book answered %v, want ErrNotPriced", err)
}
// Nothing moved: a refusal before the hold is the only kind worth having.
if acct := f.account(t); acct.Reserved != 0 {
t.Errorf("a refused purchase reserved %s", acct.Reserved.USD())
}
// ⚠ A book carrying only PART of a projection is refused for the same reason and by the same
// answer: two of three figures is a document this build did not read whole, and the one that
// would be missing is exactly the one whose absence is silent.
if _, err := f.store.Pool().Exec(f.ctx, `
update books set expected_micro_usd = 150000, book_once_micro_usd = 0 where id = $1`,
f.bookID(t)); err != nil {
t.Fatal(err)
}
if _, err := f.svc.Order(f.ctx, "u1", f.bookID(t)); !errors.Is(err, ErrNotPriced) {
t.Errorf("a book missing only its step_max was priced: %v", err)
}
}
// The order form answers the buyer's question over a REAL book, end to end: the verdict, what the
// balance covers, and the pair «expected / reserved».
func TestTheOrderFormAnswersOverARealBook(t *testing.T) {
// Five chapters at the fixture's $0.03: the whole book is affordable at $10 and not at $0.10.
rich := newFixture(t, "10", 5)
got, err := rich.svc.Order(rich.ctx, "u1", rich.bookID(t))
if err != nil {
t.Fatal(err)
}
if got.Verdict != pricing.VerdictCoversAll || got.ChaptersLeft != 5 || got.AffordableChapters != 5 {
t.Fatalf("a $10 balance over a $0.15 book: %+v", got.Options)
}
if got.Whole.Hold != fixtureHold(5) || got.Whole.Expected != fixtureChapterUSD*5 {
t.Errorf("the estimate is %s expected / %s held, want %s / %s",
got.Whole.Expected.USD(), got.Whole.Hold.USD(),
(fixtureChapterUSD * 5).USD(), fixtureHold(5).USD())
}
if got.MinHold != fixtureStepMaxUSD {
t.Errorf("the money slider's minimum is %s, want the engine's own step", got.MinHold.USD())
}
if !got.ChapterOrders || got.Structure != "detected" {
t.Errorf("a detected cut was not offered in chapters: %+v", got)
}
poor := newFixture(t, "0.15", 5)
got, err = poor.svc.Order(poor.ctx, "u1", poor.bookID(t))
if err != nil {
t.Fatal(err)
}
if got.Verdict != pricing.VerdictCoversPart || got.AffordableChapters == 0 ||
got.AffordableChapters >= 5 {
t.Fatalf("a $0.15 balance over a book that reserves %s: %+v", fixtureHold(5).USD(), got.Options)
}
}
// The three kinds of order end up as three DIFFERENT boundaries on the book row, and each is an
// identity. The chapter number rides beside its identity as a LABEL — which is what makes a shifted
// number visible rather than authoritative.
func TestEachKindOfOrderStoresItsOwnBoundaryAsAnIdentity(t *testing.T) {
f := newFixture(t, "10", 5)
book := f.bookID(t)
read := func() (chapterID, unitID *string, number *int) {
t.Helper()
if err := f.store.Pool().QueryRow(f.ctx, `
select ordered_through_chapter_id, ordered_through_unit_id, ordered_through_chapter_number
from books where id = $1`, book).Scan(&chapterID, &unitID, &number); err != nil {
t.Fatal(err)
}
return
}
run, err := f.svc.Start(f.ctx, StartRequest{UserID: "u1", BookID: book, Chapters: order(3)})
if err != nil {
t.Fatal(err)
}
chapterID, unitID, number := read()
if chapterID == nil || *chapterID != "c3" || unitID != nil || number == nil || *number != 3 {
t.Fatalf("a chapter order stored chapter=%v unit=%v number=%v", chapterID, unitID, number)
}
f.finish(t, run.ID)
// A CHARACTER order: the fixture's units are 1000 runes each, so 2500 buys three of them —
// rounded UP to the unit that contains the 2500th rune.
chars := int64(2500)
run, err = f.svc.Start(f.ctx, StartRequest{UserID: "u1", BookID: book, Characters: &chars})
if err != nil {
t.Fatal(err)
}
chapterID, unitID, number = read()
if unitID == nil || *unitID != "u3" || chapterID != nil {
t.Fatalf("a character order stored chapter=%v unit=%v", chapterID, unitID)
}
if number != nil {
t.Errorf("a character order invented a chapter label: %v", number)
}
f.finish(t, run.ID)
// The WHOLE BOOK: no boundary of any kind, which is the only form that keeps meaning the whole
// book after a re-cut.
if _, err := f.svc.Start(f.ctx, StartRequest{UserID: "u1", BookID: book}); err != nil {
t.Fatal(err)
}
if chapterID, unitID, _ = read(); chapterID != nil || unitID != nil {
t.Errorf("the whole book stored chapter=%v unit=%v", chapterID, unitID)
}
}
// A character order over a book whose chapters ARE recognised still resolves to units, and an order
// in CHAPTERS over a book whose cut cannot be sold against is refused with its own word rather than
// silently rounded to the whole book.
func TestAChapterOrderIsRefusedOnACutThatCannotBeSoldAgainst(t *testing.T) {
for _, structure := range []string{"none", "declared", "a word a later engine grew"} {
f := newFixture(t, "10", 5)
if _, err := f.store.Pool().Exec(f.ctx,
`update books set structure = $2 where id = $1`, f.bookID(t), structure); err != nil {
t.Fatal(err)
}
_, err := f.svc.Start(f.ctx, StartRequest{UserID: "u1", BookID: f.bookID(t), Chapters: order(2)})
if !errors.Is(err, ErrChapterOrdersUnavailable) {
t.Errorf("structure %q accepted a chapter order: %v", structure, err)
}
if acct := f.account(t); acct.Reserved != 0 {
t.Errorf("structure %q: a refused order reserved %s", structure, acct.Reserved.USD())
}
// The whole book and a character order are both still available — the refusal is of the UNIT
// the order is phrased in, not of the book.
chars := int64(1500)
if _, err := f.svc.Start(f.ctx, StartRequest{UserID: "u1", BookID: f.bookID(t), Characters: &chars}); err != nil {
t.Errorf("structure %q refused a character order too: %v", structure, err)
}
}
}
// A character order that reaches the end of the book IS the whole book, and is recorded as such: a
// boundary on the last unit would freeze an order that a re-cut then breaks for no reason.
func TestACharacterOrderThatReachesTheEndIsTheWholeBook(t *testing.T) {
f := newFixture(t, "10", 5)
chars := int64(999_999)
if _, err := f.svc.Start(f.ctx, StartRequest{UserID: "u1", BookID: f.bookID(t), Characters: &chars}); err != nil {
t.Fatal(err)
}
var unitID *string
if err := f.store.Pool().QueryRow(f.ctx,
`select ordered_through_unit_id from books where id = $1`, f.bookID(t)).Scan(&unitID); err != nil {
t.Fatal(err)
}
if unitID != nil {
t.Errorf("an order for more characters than the book has froze a boundary at %q", *unitID)
}
}
// finish closes a live run so the fixture can buy again: one live run per book by construction.
func (f *fixture) finish(t *testing.T, runID string) {
t.Helper()
if _, err := f.store.Pool().Exec(f.ctx,
`update runs set finished_at = now(), status = 'stopped' where id = $1`, runID); err != nil {
t.Fatal(err)
}
if _, err := f.store.Pool().Exec(f.ctx,
`update reservations set state = 'released', closed_at = now()
where engine_run_id like $1 and state = 'open'`, runID+"#%"); err != nil {
t.Fatal(err)
}
}
// ⛔ THE SERVER RE-JUDGES THE ORDER, and this is the half PD-375 found untested under the previous
// shape: the bound that decides HOW MUCH MONEY IS RESERVED can never be one the caller chose
// unilaterally. The options read is a READ, the balance moves under it, and a client is forbidden to
// clamp on its own account.
//
// Two halves, and both are asserted because removing either used to pass the whole battery:
// - an order the balance CANNOT carry is refused, before the hold, with nothing moved;
// - an order LARGER than the book is the whole book, not an error and not a larger reservation.
//
// Mutation caught: dropping the `quote.Hold > acct.Balance` refusal in Start; letting Quote reserve
// for more chapters than the book has.
func TestAnOrderTheBalanceCannotCarryIsRefusedBeforeTheHold(t *testing.T) {
// $0.10 against a five-chapter book whose whole order reserves more than that.
f := newFixture(t, "0.10", 5)
if fixtureHold(5) <= money.MicroUSD(100_000) {
t.Fatalf("the fixture does not test what it claims: five chapters reserve %s, which $0.10 covers",
fixtureHold(5).USD())
}
before := f.account(t)
_, err := f.svc.Start(f.ctx, StartRequest{UserID: "u1", BookID: f.bookID(t), Chapters: order(5)})
// ⚠ ITS OWN ERROR, and not ErrCeilingOutOfBounds. That one means «the options moved between the
// read and the call» and reaches the wire as `bounds_moved`, which promises a client that
// re-reading and retrying will work. Here nothing moved and retrying is futile: what is missing
// is money, and the remedy is to top up. This is the refusal the whole order form exists to make
// honest, and it used to travel under the other word.
if !errors.Is(err, ErrBalanceCannotCarry) {
t.Fatalf("an unaffordable order answered %v, want ErrBalanceCannotCarry", err)
}
if after := f.account(t); after.Reserved != before.Reserved || after.Balance != before.Balance {
t.Fatalf("money moved on a refused order: before %+v, after %+v", before, after)
}
if live, err := f.store.ListLiveRuns(f.ctx); err != nil || len(live) != 0 {
t.Fatalf("a refused order left a live run: %+v (%v)", live, err)
}
// And an order LARGER than the book is the whole book — the reservation is the book's, never the
// number the caller typed.
rich := newFixture(t, "10", 5)
if _, err := rich.svc.Start(rich.ctx, StartRequest{UserID: "u1", BookID: rich.bookID(t), Chapters: order(9999)}); err != nil {
t.Fatal(err)
}
if got := rich.account(t).Reserved; got != fixtureHold(5) {
t.Errorf("an order for 9999 chapters of a five-chapter book reserved %s, want the book's own %s",
got.USD(), fixtureHold(5).USD())
}
}
// ⛔ THE ORDER FORM'S PROMISE HAS TO SURVIVE THE CLICK, and until an adversarial pass looked it did
// not. The form answers, before the click, whether the book-wide consistency pass is funded; the
// form is a READ and the balance moves under it — another book's hold, a correction — and the hold
// taken at admission is computed against the balance AS IT IS. So a buyer shown «funded» could be
// sold a run that was not, and the passes DEGRADE rather than halt: the book arrives and only its
// terms wander. Consistency of terms across a book is the owner's first stated priority (D39.198),
// and a refusal shaped like normal work is the class D39.202 §3 names.
//
// The run carries what it actually got, so the question is answerable after the click too.
//
// Mutation caught: dropping BondFunded from the StartRunInput, or reading it from anything but the
// quote the admission acted on.
func TestWhatTheRunActuallyGotIsRecordedAndNotOnlyWhatTheFormPromised(t *testing.T) {
// A book-level bound the balance CAN carry: the run is sold with the pass funded.
f := newFixture(t, "10", 3)
if _, err := f.store.Pool().Exec(f.ctx,
`update books set book_once_micro_usd = 2000000 where id = $1`, f.bookID(t)); err != nil {
t.Fatal(err)
}
form, err := f.svc.Order(f.ctx, "u1", f.bookID(t))
if err != nil {
t.Fatal(err)
}
if !form.Whole.BondFunded {
t.Fatalf("a $10 balance does not fund a $2 book-level pass: %+v", form.Options)
}
run, err := f.svc.Start(f.ctx, StartRequest{UserID: "u1", BookID: f.bookID(t)})
if err != nil {
t.Fatal(err)
}
if !f.run(t, run.ID).BondFunded {
t.Error("a run sold with the pass funded does not say so")
}
// …and the case the form CANNOT promise: a balance that carries the chapters and not the bound.
poor := newFixture(t, "0.5", 3)
if _, err := poor.store.Pool().Exec(poor.ctx,
`update books set book_once_micro_usd = 2000000 where id = $1`, poor.bookID(t)); err != nil {
t.Fatal(err)
}
form, err = poor.svc.Order(poor.ctx, "u1", poor.bookID(t))
if err != nil {
t.Fatal(err)
}
if form.Verdict != pricing.VerdictCoversAll || form.Whole.BondFunded {
t.Fatalf("the chapters are covered and the bound is not: %+v", form.Options)
}
run, err = poor.svc.Start(poor.ctx, StartRequest{UserID: "u1", BookID: poor.bookID(t)})
if err != nil {
t.Fatal(err)
}
if poor.run(t, run.ID).BondFunded {
t.Error("a run sold WITHOUT the book-wide pass claims it is funded, which is the silence " +
"this column exists to break")
}
}
// ⛔ A WHOLE-BOOK CONTINUATION IS BOUNDED BY VOLUME TOO, and the reason is the engine's, not this
// platform's caution: with no ceiling in force the engine builds NO volume scope, and the scope is
// what carries its protective order of work — new book before re-made book. Without one, a
// continuation carrying `--resnapshot` walks the edit wave in BOOK order and re-pays the beginning
// of the book before editing the chapters just bought.
//
// That is exactly the argument this platform made for riding `--resnapshot` on every continuation
// (PD-422), and it held only for PARTIAL orders until this bound existed. Found by an adversarial
// pass reading the engine, not by a failure here.
//
// The FIRST run of a book gets no bound: nothing is delivered, so nothing can be re-made.
//
// Mutation caught: returning 0 for a whole-book order unconditionally.
func TestAWholeBookContinuationIsGivenTheRemainderAsItsVolume(t *testing.T) {
f := newFixture(t, "10", 5)
first, err := f.svc.Start(f.ctx, StartRequest{UserID: "u1", BookID: f.bookID(t)})
if err != nil {
t.Fatal(err)
}
if err := f.svc.Spawn(f.ctx, first.ID); err != nil {
t.Fatal(err)
}
if args := argvOf(t, f); slices.Contains(args, "--max-units") {
t.Fatalf("the FIRST whole-book run is bounded by volume: %q", strings.Join(args, " "))
}
// It delivers two of the five chapters and ends.
if _, err := f.store.Pool().Exec(f.ctx, `
insert into unit_resolutions (book_id, chapter, unit, wave, shipped, flagged, at)
select $1, g, 0, 'edit', true, false, now() from generate_series(1, 2) g`, f.bookID(t)); err != nil {
t.Fatal(err)
}
live := f.live(t)
if err := runner.WriteMarker(f.svc.markerPath(live.RunID, live.AttemptNo),
runner.Marker{Unit: live.UnitName, Result: "exit-code", Code: "exited", Status: "0"}); err != nil {
t.Fatal(err)
}
if err := f.svc.Sweep(f.ctx); err != nil {
t.Fatal(err)
}
second, err := f.svc.Start(f.ctx, StartRequest{UserID: "u1", BookID: f.bookID(t)})
if err != nil {
t.Fatal(err)
}
if err := f.svc.Spawn(f.ctx, second.ID); err != nil {
t.Fatal(err)
}
args := argvOf(t, f)
if got := flagValue(args, "--max-units"); got != "3" {
t.Fatalf("a whole-book CONTINUATION was given --max-units %q, want the three units still "+
"owed — without a ceiling the engine builds no scope and re-pays the beginning of the "+
"book before editing what was bought: %q", got, strings.Join(args, " "))
}
if !slices.Contains(args, "--resnapshot") {
t.Errorf("the continuation carries no --resnapshot: %q", strings.Join(args, " "))
}
}
// A stranger's book is INDISTINGUISHABLE from a missing one on the order form, exactly as it is
// everywhere else in this package: telling the two apart is what would let anyone enumerate other
// people's libraries by asking what a book costs.
//
// Pinned here because the form is a NEW path with its own query, and ownership on it is that query's
// own `where b.owner_id = $2` rather than a guard somebody remembered to put in front of it — a
// guard is exactly the thing a later refactor drops.
//
// Mutation caught: dropping `b.owner_id = $2` from ReadBookForOrder or RemainingUnits.
func TestTheOrderFormDoesNotAnswerAboutSomebodyElsesBook(t *testing.T) {
f := newFixture(t, "10", 5)
if _, err := f.store.Pool().Exec(f.ctx,
`insert into users (id, email) values ('u2','u2@example.org')`); err != nil {
t.Fatal(err)
}
if _, err := f.svc.Order(f.ctx, "u2", f.bookID(t)); !errors.Is(err, pgstore.ErrNoBook) {
t.Errorf("the order form over a stranger's book answered %v, want ErrNoBook", err)
}
if _, err := f.svc.Order(f.ctx, "u1", "bk_THISDOESNOTEXIST"); !errors.Is(err, pgstore.ErrNoBook) {
t.Errorf("the order form over a missing book answered %v, want ErrNoBook", err)
}
// The purchase path too, and it must not move money on the way to refusing.
before := f.account(t)
if _, err := f.svc.Start(f.ctx, StartRequest{UserID: "u2", BookID: f.bookID(t)}); !errors.Is(err, pgstore.ErrNoBook) {
t.Errorf("a stranger's purchase answered %v, want ErrNoBook", err)
}
if after := f.account(t); after.Reserved != before.Reserved {
t.Errorf("a stranger's refused purchase moved the owner's money: %s → %s",
before.Reserved.USD(), after.Reserved.USD())
}
// …and the character order's own read, which is a SECOND query with its own ownership clause.
if got, err := f.store.RemainingUnits(f.ctx, "u2", f.bookID(t)); err != nil || len(got) != 0 {
t.Errorf("a stranger read %d units of somebody else's book (%v)", len(got), err)
}
}
// ⛔ THE ALLOWANCE IS IN UNITS AND THE ORDER IS IN CHAPTERS, and until this test nothing in the zone
// could tell the two apart: every other fixture gives a chapter exactly one unit, so `chapters` and
// `units` were the same number everywhere and swapping one for the other stayed green.
//
// Three chapters of four units each: an order of two chapters is EIGHT units, not two.
//
// Mutation caught: `maxUnitsFor` returning `l.OrderedChapters - o.Delivered` — the unit of measure
// swapped for the one beside it.
func TestTheVolumeAllowanceCountsUnitsAndNotChapters(t *testing.T) {
f := newFixture(t, "10", 5)
book := multiUnitBook(t, f, "multi", 3, 4)
run, err := f.svc.Start(f.ctx, StartRequest{UserID: "u1", BookID: book, Chapters: order(2)})
if err != nil {
t.Fatal(err)
}
if err := f.svc.Spawn(f.ctx, run.ID); err != nil {
t.Fatal(err)
}
if got := flagValue(argvOf(t, f), "--max-units"); got != "8" {
t.Fatalf("an order of two four-unit chapters was given --max-units %q, want 8: the allowance "+
"is counted in the unit the ENGINE ships, and two is what it would be if the chapter count "+
"were handed over instead", got)
}
// …and what was SOLD is still counted in chapters, because that is what the buyer chose and what
// the run's own bar is measured in.
if got := f.run(t, run.ID).OrderedChapters; got == nil || *got != 2 {
t.Errorf("the run says it bought %v chapters", got)
}
// The hold is the units' money, not the chapters': eight units at the fixture's rate.
if got, want := f.account(t).Reserved, fixtureHold(8); got != want {
t.Errorf("the order reserved %s, want %s — the price of eight units", got.USD(), want.USD())
}
}
// ⛔ A CHARACTER ORDER'S BAR MOVES, and before this it could not: the order buys a PREFIX of a
// chapter, a chapter counts as done only once EVERY unit in it is done, so the run read `0/N` for
// its whole life and `delivered_chapters: 0` for ever.
//
// ⚠ THE PLATFORM ALREADY CALLS THAT STATE INADMISSIBLE — in its own words, in the admission that
// refuses a book with no materialised tree: «every counter the screen shows is a count over
// chapters, so the run would read 0/total for its entire life while spending» (PD-405). Introducing
// the same state through the character order would be this pack contradicting itself, and the
// character order is not a corner: `declared` is led as `none`, so today it is the ONLY partial
// purchase available for every EPUB and every non-CJK txt.
//
// Mutation caught: dropping the `r.ordered_units is not null` branch from runDone/runTotal, or
// leaving OrderedUnits nil on a character order.
func TestACharacterOrdersBarIsCountedInWhatItActuallyBought(t *testing.T) {
f := newFixture(t, "10", 5)
// One chapter of six units: the shape of a book whose chapters cannot be sold against.
book := multiUnitBook(t, f, "one-chapter", 1, 6)
chars := int64(2500) // 1000 runes a unit ⇒ three units
run, err := f.svc.Start(f.ctx, StartRequest{UserID: "u1", BookID: book, Characters: &chars})
if err != nil {
t.Fatal(err)
}
got := f.run(t, run.ID)
// Two passes over three units: the same shape the chapter bar has, one level finer.
if got.Progress.Total != 6 || got.Progress.Done != 0 {
t.Fatalf("a three-unit order opens at %d/%d, want 0/6", got.Progress.Done, got.Progress.Total)
}
if got.DeliveredChapters != nil {
t.Errorf("a run that bought a prefix of a chapter reports %d chapters delivered; it has "+
"delivered no CHAPTER, and 0 reads as «nothing happened»", *got.DeliveredChapters)
}
// The engine drafts the three it was sold. The chapter is NOT finished — three of six units — so
// the chapter bar would still read zero here, and that is the whole defect.
if _, err := f.store.Pool().Exec(f.ctx, `
insert into unit_resolutions (book_id, chapter, unit, wave, shipped, flagged, at)
select $1, 1, g - 1, 'draft', true, false, now() from generate_series(1, 3) g`, book); err != nil {
t.Fatal(err)
}
got = f.run(t, run.ID)
if got.Progress.Done != 3 {
t.Fatalf("after the draft pass over all three units the bar reads %d/%d: counted in chapters "+
"it can never move, because the chapter holds six", got.Progress.Done, got.Progress.Total)
}
if got.Progress.Stage != "editing" {
t.Errorf("the draft pass over what was bought is done and the caption says %q", got.Progress.Stage)
}
// …and the edit pass closes it.
if _, err := f.store.Pool().Exec(f.ctx, `
insert into unit_resolutions (book_id, chapter, unit, wave, shipped, flagged, at)
select $1, 1, g - 1, 'edit', true, false, now() from generate_series(1, 3) g`, book); err != nil {
t.Fatal(err)
}
if got = f.run(t, run.ID); got.Progress.Done != 6 || got.Progress.Total != 6 {
t.Errorf("a fully delivered three-unit order reads %d/%d", got.Progress.Done, got.Progress.Total)
}
}
// The ORDINARY shape is untouched: a run that bought whole chapters keeps the bar it always had,
// counted in chapters, with `delivered_chapters` a number rather than null. Asserted beside the test
// above because the unit-shaped branch is a NEW first case in three SQL expressions every existing
// bar reads, and «the new branch is not entered» is the property that keeps thirty other tests true.
func TestAChapterOrderKeepsTheBarItAlwaysHad(t *testing.T) {
f := newFixture(t, "10", 5)
book := multiUnitBook(t, f, "chapters", 3, 2)
run, err := f.svc.Start(f.ctx, StartRequest{UserID: "u1", BookID: book, Chapters: order(2)})
if err != nil {
t.Fatal(err)
}
got := f.run(t, run.ID)
if got.Progress.Total != 4 {
t.Fatalf("a two-chapter order over an editor pipeline opens with a total of %d, want 4 — two "+
"passes over two CHAPTERS, not over their four units", got.Progress.Total)
}
if got.DeliveredChapters == nil || *got.DeliveredChapters != 0 {
t.Errorf("a chapter order reports delivered chapters as %v, want 0 rather than null", got.DeliveredChapters)
}
// …and the pair that says which unit this run is measured in points the other way from the
// character order's: the chapter figure is present, the unit figure is not. Exactly one is set,
// and this is the half that would go silently null if the branch asked «is this a partial order»
// instead of «was this sold in characters».
if got.OrderedChapters == nil || *got.OrderedChapters != 2 {
t.Errorf("a two-chapter order publishes ordered_chapters %v, want 2", got.OrderedChapters)
}
if got.OrderedUnits != nil {
t.Errorf("a run sold in chapters publishes ordered_units %d; the two are exclusive", *got.OrderedUnits)
}
// One whole chapter delivered moves both the bar and the delivered count.
if _, err := f.store.Pool().Exec(f.ctx, `
update chapters set units_draft_done = 2, units_edit_done = 2
where book_id = $1 and number = 1`, book); err != nil {
t.Fatal(err)
}
got = f.run(t, run.ID)
if got.DeliveredChapters == nil || *got.DeliveredChapters != 1 {
t.Errorf("one whole chapter delivered reports %v", got.DeliveredChapters)
}
if got.Progress.Done != 2 {
t.Errorf("one whole chapter through both passes reads %d/%d", got.Progress.Done, got.Progress.Total)
}
}
// ⛔ A CHARACTER ORDER PUBLISHES ITS VOLUME IN THE UNIT IT WAS SOLD IN, and says NOTHING in chapters.
//
// ⚠ THIS TEST REPLACES ONE THAT PINNED THE OPPOSITE, and the replacement is ordered rather than
// convenient (D39.183 — declared in the pack's report). Its predecessor,
// `TestACharacterOrderReportsTheChapterSpanItReachesIntoAndNotZero`, pinned the SPAN — how many
// chapters the order reaches into — because that was what the build published and the contract
// described a zero. Measuring it is what killed it: the span OVERSTATES what was bought by as much as
// a whole chapter, and worst on the cheapest order there is. One unit out of four in the first
// chapter published «1 chapter», which a person reads as «my money reached the end of chapter one».
// The guarantee did not disappear — it moved: what the buyer got is now published as `ordered_units`,
// in the unit they asked in, and `ordered_chapters` is null, which is the honest answer to a question
// that does not apply.
//
// ⚠ The row underneath still holds the span — see pgstore.runOrderedChapters — because
// `ceiling_chapters = 0` is the RE-PASS's mark and three internal readers depend on it.
//
// Mutation caught: publishing the span again; nulling BOTH figures; nulling the chapter figure for a
// CHAPTER order (the branch is `ordered_units is not null`, not «is this a partial order»).
func TestACharacterOrdersVolumeIsPublishedInTheUnitItWasSoldIn(t *testing.T) {
f := newFixture(t, "10", 500)
book := multiUnitBook(t, f, "span", 3, 4) // three chapters, four units each, 1000 runes a unit
// 4500 runes ⇒ five units ⇒ the whole of chapter one and one unit of chapter two.
chars := int64(4500)
run, err := f.svc.Start(f.ctx, StartRequest{UserID: "u1", BookID: book, Characters: &chars})
if err != nil {
t.Fatal(err)
}
got := f.run(t, run.ID)
if got.OrderedChapters != nil {
t.Errorf("a run sold in characters reports ordered_chapters %d: the chapter figure it used to "+
"publish was the SPAN it reached into, and it overstated what was bought", *got.OrderedChapters)
}
if got.OrderedUnits == nil || *got.OrderedUnits != 5 {
t.Errorf("a five-unit order publishes ordered_units %v, want 5 — the unit it was sold in", got.OrderedUnits)
}
// …and the two figures that DO speak the order's own unit say so, which is what makes the span
// readable rather than misleading: the bar counts units and no chapter is claimed as delivered.
if got.Progress.Total != 10 {
t.Errorf("the bar of a five-unit order over two waves reads /%d, want /10", got.Progress.Total)
}
if got.DeliveredChapters != nil {
t.Errorf("delivered_chapters is %d rather than null on a unit-shaped run", *got.DeliveredChapters)
}
// ⚠ AND THE ORDER CLOSED A WHOLE CHAPTER — chapter one, all four of its units — while still being
// unit-shaped. So «unit-shaped» means «phrased in characters», NOT «closes no whole chapter»,
// which is how three doc comments and the canon describe it.
if got.Progress.Done != 0 {
t.Errorf("nothing has been delivered yet and the bar reads %d", got.Progress.Done)
}
}
// ⛔ A BOOK WITH NOTHING LEFT IS REFUSED, AND THAT REFUSAL IS WHAT MAKES `ordered_chapters: 0` MEAN
// «RE-PASS» AND NOTHING ELSE.
//
// The contract now tells a client to read the bar's unit off that field, and its FIRST case is «zero
// ⇒ a re-pass, `total` is 1, do not render it as chapters». That rule is only safe because no other
// shape can put a zero there — and one nearly can: `pricing.Quote` clamps a whole-book order to what
// remains, so over a fully delivered book it quotes ZERO chapters, and the store's own admission
// check refuses a zero only when `Resnapshot` is false, which a continuation over a moved bank sets
// to true. The guard in `Start` is what closes that door, ahead of the store.
//
// Without it such a run would land with `ceiling_chapters = 0` and be INDISTINGUISHABLE FROM A
// RE-PASS on the wire: bar 0/1, `stage: re_pass`, `delivered_chapters: 0`. The buyer would be shown
// somebody else's shape of work.
//
// Mutation caught: dropping the `quote.Chapters == 0` guard; moving it after the hold is taken;
// letting Quote return the remainder count without clamping.
func TestABookWithNothingLeftIsRefusedRatherThanSoldAsAZeroChapterRun(t *testing.T) {
f := newFixture(t, "10", 500)
book := multiUnitBook(t, f, "done", 3, 2)
// Every unit of every chapter delivered in the wave the book is measured in.
if _, err := f.store.Pool().Exec(f.ctx, `
insert into unit_resolutions (book_id, chapter, unit, wave, shipped, flagged, at)
select $1, c, u - 1, 'edit', true, false, now()
from generate_series(1, 3) c, generate_series(1, 2) u`, book); err != nil {
t.Fatal(err)
}
_, err := f.svc.Start(f.ctx, StartRequest{UserID: "u1", BookID: book})
if err == nil {
t.Fatal("a fully delivered book was sold a run; on the wire it would be indistinguishable " +
"from a re-pass, because both carry ordered_chapters 0")
}
if !errors.Is(err, ErrCeilingOutOfBounds) {
t.Errorf("the refusal came as %v; the client needs the one that means «re-read the options»", err)
}
// …and the money did not move: the refusal is ahead of the hold.
if got := f.account(t).Reserved; got != 0 {
t.Errorf("a refused order reserved %s", got.USD())
}
}