textmachine/platform/internal/pgstore/spendbound_test.go

86 lines
4.3 KiB
Go

package pgstore
import (
"context"
"testing"
"time"
"textmachine/platform/internal/money"
)
// spendbound_test.go: the word `min` in SpendBound, executed as a CHOICE.
//
// SpendBound says "the SMALLEST meter reading any LATER attempt of the same book recorded", and the
// smallest is the only correct one: a later reading already contains the spend of everything
// between, so bounding by a bigger one charges this attempt for its successor's work, which the
// successor then pays for again — PD-159, measured at $0.10 charged as $2.10.
//
// The pin PD-159 names (runs.TestADeferredSettlementIsNotChargedForTheNextRunOfTheSameBook) puts
// exactly ONE later attempt in the book, and over a set of one `min` and `max` are the same row — so
// nothing in the battery executed the word. Measured: `min` -> `max` in SpendBound leaves the FULL
// battery green (register row PD-376). Here there are three later readings and the smallest is
// neither the first nor the last of them, so "the first later one" and "the last later one" fail it
// as well.
//
// Taken from the review pack's own draft (docs/p8-review/axis1-money/r1_spendbound_test.go.txt) in
// preference to its sibling a1_: that one raw-INSERTs its runs and its smallest baseline happens to
// be the first later attempt, so an "order by id limit 1" mutant would survive it.
//
// Mutations caught: `min` -> `max`; `min` -> the first later attempt; dropping the `> $2` that makes
// "later" mean later.
func TestTheSpendBoundIsTheSmallestLaterBaselineAndNotMerelyALaterOne(t *testing.T) {
s, ctx := testDB(t)
now := fundedAccount(t, s, ctx, "u1", "10")
seedBook(t, s, ctx, "bk1", "u1", 500)
// A SECOND book of the same account, whose attempts are later still and cheaper than any of
// bk1's. Without it the bound is the smallest later baseline in the DATABASE and the smallest
// later baseline of THIS BOOK by coincidence, so dropping `r.book_id = $1` from the query — which
// would charge one book's settlement against another book's meter — passes unnoticed.
seedBook(t, s, ctx, "bk2", "u1", 500)
subject := attemptWithBaseline(t, s, ctx, "bk1", now, 50_000)
// Three attempts start AFTER it. Their baselines are deliberately out of id order: the smallest
// sits in the MIDDLE, so neither "the first later one" nor "the last later one" is it.
attemptWithBaseline(t, s, ctx, "bk1", now, 900_000)
attemptWithBaseline(t, s, ctx, "bk1", now, 200_000)
attemptWithBaseline(t, s, ctx, "bk1", now, 700_000)
// Later than all of them and smaller than all of them — and about ANOTHER book.
attemptWithBaseline(t, s, ctx, "bk2", now, 1_000)
bound, err := s.SpendBound(ctx, "bk1", subject)
if err != nil {
t.Fatal(err)
}
if bound == nil {
t.Fatal("no bound at all, want the smallest later baseline: the fixture is not the one this test claims")
}
if want := money.MicroUSD(200_000); *bound != want {
t.Fatalf("the bound is %s, want %s — the SMALLEST later baseline OF THIS BOOK. A larger one "+
"already contains what the attempts between it and this one spent, so this settlement "+
"pays for their work and they pay for it again; another BOOK's baseline is not a bound "+
"on this one at all", bound.USD(), want.USD())
}
}
// attemptWithBaseline admits one run on bk1, records a spawn for it carrying the given book-meter
// reading, then finishes the run so the next one can be admitted (runs_one_live_per_book).
//
// Through the ordinary doors and not raw INSERTs: what is under test is a query over rows the
// platform itself writes, and a fixture that writes them by hand can agree with a query that no
// producer would ever satisfy.
func attemptWithBaseline(t *testing.T, s *Store, ctx context.Context, bookID string, now time.Time, baseline money.MicroUSD) int64 {
t.Helper()
run, err := s.StartRun(ctx, StartRunInput{UserID: "u1", BookID: bookID, CeilingChapters: 10,
Ceiling: money.MicroUSD(300_000), Now: now}, 0, nil)
if err != nil {
t.Fatal(err)
}
claimed, err := s.RecordSpawn(ctx, SpawnRecord{AttemptID: run.AttemptID, Unit: "tm-run-" + run.ID,
Binary: "/opt/engine/1/tmctl", Ceiling: 300_000, CeilingArg: 300_000 + baseline, Baseline: baseline})
if err != nil || !claimed {
t.Fatalf("record spawn: claimed=%v err=%v", claimed, err)
}
exec(t, s, ctx, `update runs set finished_at = $2, status = 'ready' where id = $1`, run.ID, now)
return run.AttemptID
}