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 DeliveredUnits. 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 != 2 { t.Errorf("the run says it bought %d 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) } // 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) } }