package httpapi import ( "bytes" "context" "encoding/json" "errors" "log/slog" "net/http" "net/http/httptest" "strings" "testing" "time" "textmachine/platform/internal/auth" "textmachine/platform/internal/pgstore" "textmachine/platform/internal/pricing" "textmachine/platform/internal/runner" "textmachine/platform/internal/runs" ) // These tests assert the WIRE, field for field, because the wire is the contract (openapi 0.2.0) and // a projection is exactly the kind of code that drifts from it silently: nothing in Go fails when a // json tag is misspelled, and the client that breaks is in another repository. type fakeLibrary struct { lib pgstore.Library book pgstore.Book run *pgstore.Run usage pgstore.Usage chapters pgstore.ChapterPage units pgstore.UnitPage notes pgstore.NotePage bank pgstore.BankPage stream pgstore.StreamState frames []pgstore.Frame // onSecondRead mutates the state the SECOND time a stream asks for it, which is how a test says // "this changed while the client was connected". onSecondRead func(*pgstore.StreamState) streamReads int // limit records the page size the handler passed DOWN, so a test can observe the clamp rather // than only the status a refusal would have changed. limit int err error // renamedTo and renames are what the rename door did, so a test can tell "answered 400" from // "answered 400 after writing". renamedTo string // renamedUser and renamedBook are the ids the handler PASSED DOWN. Without them the ownership // assertion tests only that the store was called, and a handler reading the wrong path value — // or the wrong principal — keeps the whole battery green. renamedUser string renamedBook string renames int } func (f *fakeLibrary) ListBooks(context.Context, string, int, string) (pgstore.Library, error) { return f.lib, f.err } func (f *fakeLibrary) GetBook(context.Context, string, string) (pgstore.Book, *pgstore.Run, error) { return f.book, f.run, f.err } // renamedTo records the title the handler passed DOWN, so a test can observe what would be stored // rather than only the status the answer carried. func (f *fakeLibrary) RenameBook(_ context.Context, userID, bookID, title string) (pgstore.Book, *pgstore.Run, error) { f.renamedUser, f.renamedBook, f.renamedTo = userID, bookID, title f.renames++ if f.err != nil { return pgstore.Book{}, nil, f.err } b := f.book b.Title = title return b, f.run, nil } func (f *fakeLibrary) ReadUsage(context.Context, string) (pgstore.Usage, error) { return f.usage, f.err } func (f *fakeLibrary) ListChapters(_ context.Context, _, _ string, limit int, _ string) (pgstore.ChapterPage, error) { f.limit = limit return f.chapters, f.err } func (f *fakeLibrary) ListUnits(context.Context, string, string, string, int, string) (pgstore.UnitPage, error) { return f.units, f.err } func (f *fakeLibrary) ListNotes(context.Context, string, string, int, string, *int64) (pgstore.NotePage, error) { return f.notes, f.err } func (f *fakeLibrary) ListBank(context.Context, string, string, int, string, *int64) (pgstore.BankPage, error) { return f.bank, f.err } func (f *fakeLibrary) ReadStream(context.Context, string, string) (pgstore.StreamState, error) { f.streamReads++ if f.streamReads == 2 && f.onSecondRead != nil { f.onSecondRead(&f.stream) } return f.stream, f.err } // The `after` is honoured rather than ignored: a fake that answered the same frames on every poll // would hide exactly the bug a stream test is looking for — a watermark that does not move. func (f *fakeLibrary) ReadFrames(_ context.Context, _ string, after int64, limit int) ([]pgstore.Frame, error) { var out []pgstore.Frame for _, fr := range f.frames { if fr.Position > after && len(out) < limit { out = append(out, fr) } } return out, f.err } type fakeRuns struct { bounds runs.Options run pgstore.Run err error got runs.StartRequest // stopped and resumed record which run each control handle was called for, so a test can assert // that the path parameter reaches the service rather than only that the status code is right. stopped string resumed string } func (f *fakeRuns) Order(context.Context, string, string) (runs.Options, error) { return f.bounds, f.err } func (f *fakeRuns) Start(_ context.Context, in runs.StartRequest) (pgstore.Run, error) { f.got = in return f.run, f.err } func (f *fakeRuns) Stop(_ context.Context, _, runID string) (pgstore.Run, error) { f.stopped = runID return f.run, f.err } func (f *fakeRuns) Resume(_ context.Context, _, runID string) (pgstore.Run, error) { f.resumed = runID return f.run, f.err } func v0Server(t *testing.T, lib Library, rn Runs) http.Handler { t.Helper() return v0ServerWith(t, Deps{Library: lib, Runs: rn}) } // v0ServerWith builds the real handler chain with whatever this test wants mounted on it. Every // wire test goes through New — not through a handler in isolation — because the guard, the body cap // and the security headers are part of the surface being asserted. func v0ServerWith(t *testing.T, d Deps) http.Handler { t.Helper() d.Log = slog.New(slog.NewJSONHandler(&bytes.Buffer{}, nil)) if d.Auth == nil { // Filled in rather than forced, so a test about what happens to an ALREADY authenticated // caller can bring its own store — the guard's verdict and the session's later life are two // different questions (PD-379). d.Auth = &auth.Authenticator{ Sessions: liveSessions{}, IdleTTL: time.Hour, Deny: ProblemHandler(CodeUnauthenticated), } } h, err := New(d) if err != nil { t.Fatal(err) } return h } func call(t *testing.T, h http.Handler, method, path, body string) *httptest.ResponseRecorder { t.Helper() var r *http.Request if body == "" { r = httptest.NewRequest(method, path, nil) } else { r = httptest.NewRequest(method, path, strings.NewReader(body)) r.Header.Set("Content-Type", "application/json") } r.Header.Set("Authorization", "Bearer token") w := httptest.NewRecorder() h.ServeHTTP(w, r) return w } func decode(t *testing.T, w *httptest.ResponseRecorder) map[string]any { t.Helper() var m map[string]any if err := json.Unmarshal(w.Body.Bytes(), &m); err != nil { t.Fatalf("body %q: %v", w.Body.String(), err) } return m } func TestTheLibraryResponseCarriesEveryRequiredField(t *testing.T) { lib := &fakeLibrary{lib: pgstore.Library{ Revision: 1841, Books: []pgstore.Book{{ ID: "bk_7c1", Revision: 1840, Title: "蛊真人", SourceLang: "zh", TargetLang: "ru", Status: "translating", StructureVersion: 3, ChapterCount: 500, ChaptersDone: 7, CharacterCount: 23_000_000, NoteCount: 3, AddedAt: time.Unix(0, 0).UTC(), }}, }} w := call(t, v0Server(t, lib, &fakeRuns{}), "GET", "/v0/books", "") if w.Code != http.StatusOK { t.Fatalf("status %d: %s", w.Code, w.Body) } got := decode(t, w) for _, k := range []string{"revision", "next_cursor", "books"} { if _, ok := got[k]; !ok { t.Errorf("Library is missing the required field %q", k) } } // next_cursor is present on EVERY list response, and null on the last page: introducing it later // would silently cut the tail off a client that does not read the field. if got["next_cursor"] != nil { t.Errorf("next_cursor on the last page is %v, want null", got["next_cursor"]) } books, _ := got["books"].([]any) if len(books) != 1 { t.Fatalf("books: %v", got["books"]) } book, _ := books[0].(map[string]any) for _, k := range []string{"id", "revision", "title", "source_lang", "target_lang", "status", "reject_reason", "structure_version", "chapter_count", "chapters_done", "character_count", "added_at", "note_count"} { if _, ok := book[k]; !ok { t.Errorf("Book is missing the required field %q", k) } } // ⚠ The bar is the RUN's since 0.3.0, and the book's own figure is chapters_done. A `progress` // on the library row would be the pipeline's phases back on the wire. if _, ok := book["progress"]; ok { t.Error("the library row carries a progress bar; the bar belongs to the run") } if book["chapters_done"] != float64(7) || book["structure_version"] != float64(3) { t.Errorf("book row: %v", book) } // Not a wave name, not an engine word anywhere on THIS wire — the library row, which carries no // bar by construction. (`Progress.stage` on the RUN legally says drafting/editing since 0.6.0; // this list guards the surface that must stay free of all of it.) for _, leak := range []string{"draft", "edit", "wave", "stage", "mined", "ruby", "genre"} { if bytes.Contains(w.Body.Bytes(), []byte(leak)) { t.Errorf("the pipeline's vocabulary reached the wire: %q in %s", leak, w.Body) } } // ⚠ THE REASON UNDER THIS ASSERTION CHANGED ON 05.09 AND THE ASSERTION DID NOT. It used to read // «money never crosses this boundary in any form (D39.84)» — a prohibition the OWNER revoked // (D39.196 §2, errata 05.09-а): the balance, the order's ceiling and the hold now go out in // dollars, on the ORDER FORM, which is a different wire and has its own tests. // // What is asserted here is narrower and still true: the LIBRARY ROW carries no money. A library // page is a list of books, and a sum on it would be a figure with no order behind it to explain // what it is a price OF — the same reason this row carries no progress bar. if bytes.Contains(w.Body.Bytes(), []byte("usd")) || bytes.Contains(w.Body.Bytes(), []byte("micro")) { t.Errorf("a money field reached the library row: %s", w.Body) } } // Absent, not zero: the screen must render without an estimate rather than show "0 s left". func TestAnAbsentEtaTravelsAsNullAndNotAsZero(t *testing.T) { lib := &fakeLibrary{book: pgstore.Book{ID: "bk_1"}, run: &pgstore.Run{ID: "run_1", Status: "translating"}} got := decode(t, call(t, v0Server(t, lib, &fakeRuns{}), "GET", "/v0/books/bk_1", "")) run, _ := got["run"].(map[string]any) progress, _ := run["progress"].(map[string]any) if v, ok := progress["eta_seconds"]; !ok || v != nil { t.Errorf("eta_seconds = %v (present: %v), want an explicit null", v, ok) } } func TestTheBookCardCarriesItsRunOrAnExplicitNull(t *testing.T) { lib := &fakeLibrary{book: pgstore.Book{ID: "bk_1", Revision: 12}} w := call(t, v0Server(t, lib, &fakeRuns{}), "GET", "/v0/books/bk_1", "") got := decode(t, w) if v, ok := got["run"]; !ok || v != nil { t.Errorf("run = %v (present: %v), want an explicit null for a book that never ran", v, ok) } if got["revision"] != float64(12) { t.Errorf("a card without a run carries revision %v, want the book's own", got["revision"]) } finished := time.Unix(0, 0).UTC() lib.book.Revision = 1900 lib.run = &pgstore.Run{ID: "run_1", BookID: "bk_1", Revision: 12, Status: "paused", VerifyBank: true, OrderedChapters: ptr(100), Progress: pgstore.Progress{Done: 40, Total: 100}, PausedReason: "credit_exhausted", StartedAt: finished, FinishedAt: &finished} got = decode(t, call(t, v0Server(t, lib, &fakeRuns{}), "GET", "/v0/books/bk_1", "")) run, _ := got["run"].(map[string]any) for _, k := range []string{"id", "book_id", "revision", "status", "stop_for_signing", "stop_requested", "ordered_chapters", "delivered_chapters", "progress", "paused_reason", "failure_reason", "started_at", "finished_at"} { if _, ok := run[k]; !ok { t.Errorf("Run is missing the required field %q", k) } } if run["paused_reason"] != "credit_exhausted" || run["status"] != "paused" { t.Errorf("a paused run: %v", run) } // `stop_requested` is a TOTAL function: no stop is `false`, never an absent field. It answers // what no status can — a stop asked for during translation can meet the run reaching the bank // signature, and `awaiting_bank` then offers to continue on a click that meant "stop". if run["stop_requested"] != false { t.Errorf("a run nobody stopped reports stop_requested=%v", run["stop_requested"]) } lib.run.StopRequested = true asked := decode(t, call(t, v0Server(t, lib, &fakeRuns{}), "GET", "/v0/books/bk_1", ""))["run"].(map[string]any) if asked["stop_requested"] != true { t.Errorf("a run the user asked to stop reports stop_requested=%v", asked["stop_requested"]) } // The BOOK's counter, not the run's: one counter per book (contract §Revision). if got["revision"] != float64(1900) { t.Errorf("a card with a run carries revision %v", got["revision"]) } } // paused_reason is required and null in every state but `paused`: a client that had to tell "absent" // from "null" would carry a branch the contract does not describe. func TestPausedReasonIsNullRatherThanAbsentOnALiveRun(t *testing.T) { lib := &fakeLibrary{book: pgstore.Book{ID: "bk_1"}, run: &pgstore.Run{ID: "run_1", Status: "translating"}} got := decode(t, call(t, v0Server(t, lib, &fakeRuns{}), "GET", "/v0/books/bk_1", "")) run, _ := got["run"].(map[string]any) v, ok := run["paused_reason"] if !ok || v != nil { t.Errorf("paused_reason = %v (present: %v)", v, ok) } if v, ok := run["finished_at"]; !ok || v != nil { t.Errorf("finished_at = %v (present: %v)", v, ok) } } // The order form answers the buyer's question rather than handing them arithmetic: the verdict, what // the balance covers, the pair «expected / reserved», and the money slider's own bounds. // // ⚠ MONEY IS ON THIS WIRE NOW, and it is here deliberately. The prohibition «no money fields in the // UI at all» was the owner's own and the owner REVOKED it on 05.09 (D39.196 §2, errata 05.09-а): // the account balance, the order's ceiling and the hold go out in DOLLARS. What stayed forbidden is // a different list and it was not revoked — model prices, the cost of stages and of calls, the shape // of OUR spending (ПТ-33) — and none of it is here. func TestTheOrderFormCarriesTheVerdictTheEstimateAndTheSlidersBounds(t *testing.T) { rn := &fakeRuns{bounds: runs.Options{ Options: pricing.Options{ ChaptersLeft: 166, AffordableChapters: 166, Verdict: pricing.VerdictCoversAll, Balance: 25_000_000, MinHold: 69_828, Whole: pricing.Quote{Chapters: 166, Expected: 8_300_000, Hold: 10_444_828, BondFunded: true}, }, Structure: "detected", ChapterOrders: true, SourceChars: 412_000, }} got := decode(t, call(t, v0Server(t, &fakeLibrary{}, rn), "GET", "/v0/books/bk_1/run-options", "")) order, _ := got["order"].(map[string]any) if order["chapters_left"] != float64(166) || order["affordable_chapters"] != float64(166) || order["verdict"] != "covers_all" { t.Fatalf("the order: %v", order) } if order["chapter_orders"] != true || order["structure"] != "detected" || order["source_chars"] != float64(412_000) { t.Errorf("the cut's provenance did not reach the client: %v", order) } if order["term_consistency_funded"] != true { t.Errorf("the book-wide consistency pass reported as unfunded: %v", order) } estimate, _ := order["estimate"].(map[string]any) if estimate["expected_micro_usd"] != float64(8_300_000) || estimate["hold_micro_usd"] != float64(10_444_828) { t.Errorf("the estimate is a PAIR — expected and reserved — and came out as %v", estimate) } if got["balance_micro_usd"] != float64(25_000_000) { t.Errorf("the balance: %v", got["balance_micro_usd"]) } limit, _ := got["limit"].(map[string]any) // The minimum is ONE indivisible reservation, so the slider cannot express an order under which // no run could move at all; the maximum is the balance as it is. if limit["min_micro_usd"] != float64(69_828) || limit["max_micro_usd"] != float64(25_000_000) { t.Errorf("the money slider's bounds: %v", limit) } } // A book whose chapter cut cannot be sold against says so, and says WHY, instead of offering a // chapter slider over boundaries that do not name chapters. ⚠ `declared` is deliberately among them: // for an EPUB the engine cuts by spine DOCUMENTS, and «document» is not «chapter» (ratified 05.09). func TestABookWhoseCutCannotBeSoldAgainstOffersCharactersAndSaysSo(t *testing.T) { for _, structure := range []string{"none", "declared", "something_the_engine_grew_later"} { rn := &fakeRuns{bounds: runs.Options{ Options: pricing.Options{ChaptersLeft: 1, AffordableChapters: 1, Verdict: pricing.VerdictCoversAll}, Structure: structure, ChapterOrders: false, SourceChars: 90_000, }} got := decode(t, call(t, v0Server(t, &fakeLibrary{}, rn), "GET", "/v0/books/bk_1/run-options", "")) order, _ := got["order"].(map[string]any) if order["chapter_orders"] != false { t.Errorf("structure %q: chapter orders offered", structure) } if order["structure"] != structure { t.Errorf("structure %q reached the client as %v", structure, order["structure"]) } if order["source_chars"] != float64(90_000) { t.Errorf("structure %q: no character figure to order against: %v", structure, order) } } // Nothing said yet is `null` and not an invented word: a book whose manifest has not been read // has no provenance, which is a different fact from having a poor one. rn := &fakeRuns{bounds: runs.Options{Options: pricing.Options{Verdict: pricing.VerdictCoversNone}}} order, _ := decode(t, call(t, v0Server(t, &fakeLibrary{}, rn), "GET", "/v0/books/bk_1/run-options", ""))["order"].(map[string]any) if v, ok := order["structure"]; !ok || v != nil { t.Errorf("structure = %v (present %v), want an explicit null", v, ok) } } // An unfunded book-wide consistency pass must be VISIBLE before the click. It degrades rather than // halts, so the book still arrives and only its terms wander — a refusal shaped like normal work, // which is the class D39.202 §3 names, in the most expensive place there is. func TestAnUnfundedConsistencyPassIsSaidOutLoudRatherThanPassingAsCoversAll(t *testing.T) { rn := &fakeRuns{bounds: runs.Options{ Options: pricing.Options{ ChaptersLeft: 5, AffordableChapters: 5, Verdict: pricing.VerdictCoversAll, Whole: pricing.Quote{Chapters: 5, Expected: 250_000, Hold: 382_328, BondFunded: false}, }, ChapterOrders: true, Structure: "detected", }} order, _ := decode(t, call(t, v0Server(t, &fakeLibrary{}, rn), "GET", "/v0/books/bk_1/run-options", ""))["order"].(map[string]any) if order["verdict"] != "covers_all" { t.Fatalf("the chapters ARE covered: %v", order) } if order["term_consistency_funded"] != false { t.Error("an order that cannot fund the book-wide pass passed as fully covered, in silence") } } // `covers_none` with `chapters_left: 0` is a FINISHED book, and with a positive count it is an // account that cannot buy the next chapter. One word, told apart by the count beside it. func TestAnExhaustedAccountAndAFinishedBookAreToldApartByTheCount(t *testing.T) { empty := &fakeRuns{bounds: runs.Options{Options: pricing.Options{ChaptersLeft: 0, Verdict: pricing.VerdictCoversNone}}} order, _ := decode(t, call(t, v0Server(t, &fakeLibrary{}, empty), "GET", "/v0/books/bk_1/run-options", ""))["order"].(map[string]any) if order["verdict"] != "covers_none" || order["chapters_left"] != float64(0) { t.Errorf("a finished book: %v", order) } broke := &fakeRuns{bounds: runs.Options{Options: pricing.Options{ChaptersLeft: 40, Verdict: pricing.VerdictCoversNone}}} order, _ = decode(t, call(t, v0Server(t, &fakeLibrary{}, broke), "GET", "/v0/books/bk_1/run-options", ""))["order"].(map[string]any) if order["verdict"] != "covers_none" || order["chapters_left"] != float64(40) { t.Errorf("an account with nothing to spend: %v", order) } } // The THREE kinds of order, and the default among them. ⚠ A request naming no volume is the WHOLE // BOOK — the ratified default (D39.196 §1, «не тронута ни одна ручка ⇒ заказ = вся книга») — and not // a missing field: what `ceiling_chapters` used to guard is now guarded by the hold, which the // server computes from the order it resolved itself. func TestARunTakesTheThreeKindsOfOrderAndDefaultsToTheWholeBook(t *testing.T) { rn := &fakeRuns{run: pgstore.Run{ID: "run_1", Status: "translating", OrderedChapters: ptr(100), VerifyBank: true, StartedAt: time.Unix(0, 0).UTC()}} h := v0Server(t, &fakeLibrary{}, rn) // The whole book: no volume named at all. w := call(t, h, "POST", "/v0/books/bk_1/runs", `{"stop_for_signing":true}`) if w.Code != http.StatusAccepted { t.Fatalf("a whole-book order answered %d: %s", w.Code, w.Body) } if rn.got.Chapters != nil || rn.got.Characters != nil || !rn.got.VerifyBank || rn.got.BookID != "bk_1" || rn.got.UserID != "u1" { t.Errorf("the whole-book order reached the service as %+v", rn.got) } // Chapters. if w := call(t, h, "POST", "/v0/books/bk_1/runs", `{"stop_for_signing":true,"chapters":12}`); w.Code != http.StatusAccepted { t.Fatalf("a chapter order answered %d: %s", w.Code, w.Body) } if rn.got.Chapters == nil || *rn.got.Chapters != 12 || rn.got.Characters != nil { t.Errorf("the chapter order reached the service as %+v", rn.got) } // Characters. if w := call(t, h, "POST", "/v0/books/bk_1/runs", `{"stop_for_signing":false,"characters":50000}`); w.Code != http.StatusAccepted { t.Fatalf("a character order answered %d: %s", w.Code, w.Body) } if rn.got.Characters == nil || *rn.got.Characters != 50_000 || rn.got.Chapters != nil { t.Errorf("the character order reached the service as %+v", rn.got) } got := decode(t, call(t, h, "POST", "/v0/books/bk_1/runs", `{"stop_for_signing":true,"chapters":100}`)) if got["id"] != "run_1" { t.Errorf("the accepted run: %v", got) } } // What IS still refused at the door, and each for its own reason: the one required member, a body // that is not JSON, two volumes at once (guessing which was meant is the quiet half-belief this // surface refuses everywhere else), and a volume beside a re-pass, which buys none. func TestAMalformedOrderIsRefusedRatherThanGuessed(t *testing.T) { rn := &fakeRuns{run: pgstore.Run{ID: "run_1", Status: "translating", StartedAt: time.Unix(0, 0).UTC()}} h := v0Server(t, &fakeLibrary{}, rn) for _, body := range []string{ `{}`, `not json`, `{"stop_for_signing":true,"chapters":12,"characters":5000}`, `{"stop_for_signing":true,"re_pass":true,"chapters":12}`, `{"stop_for_signing":true,"chapters":0}`, `{"stop_for_signing":true,"chapters":-3}`, `{"stop_for_signing":true,"characters":0}`, } { if w := call(t, h, "POST", "/v0/books/bk_1/runs", body); w.Code != http.StatusBadRequest { t.Errorf("body %s answered %d, want 400", body, w.Code) } } } // The contract's status codes, and the one deliberate departure from them, named out loud. func TestDomainFailuresBecomeTheContractsStatusCodes(t *testing.T) { cases := []struct { name string err error want int }{ {"a book that is not this account's", pgstore.ErrNoBook, http.StatusNotFound}, {"a book already being translated", pgstore.ErrRunInFlight, http.StatusConflict}, // 409 and not 400: the request was legal when run-options was read, and a hold taken for // another book between that read and this call is what moved the bounds. {"a ceiling that no longer fits", runs.ErrCeilingOutOfBounds, http.StatusConflict}, {"an account that cannot pay", pgstore.ErrInsufficientCredit, http.StatusConflict}, // ⚠ 503 is NOT among the statuses the contract enumerates for this operation. It is used // because every alternative lies: the request is valid, the object exists, and the state is // not in conflict — the DEPLOYMENT cannot tell the engine its ceiling (row 145). {"a deployment that cannot pass a ceiling", runner.ErrCeilingNotWired, http.StatusServiceUnavailable}, } for _, tc := range cases { h := v0Server(t, &fakeLibrary{}, &fakeRuns{err: tc.err}) w := call(t, h, "POST", "/v0/books/bk_1/runs", `{"stop_for_signing":false,"chapters":10}`) if w.Code != tc.want { t.Errorf("%s: %d, want %d", tc.name, w.Code, tc.want) } if ct := w.Header().Get("Content-Type"); ct != "application/problem+json" { t.Errorf("%s: content type %q", tc.name, ct) } // Neither title nor detail may carry engine or database text: what a client puts on a screen // is whatever this says. if bytes.Contains(w.Body.Bytes(), []byte("pgstore")) || bytes.Contains(w.Body.Bytes(), []byte("runner:")) { t.Errorf("%s: internals reached the wire: %s", tc.name, w.Body) } } } func TestUsageCarriesAShareAndItsState(t *testing.T) { for _, tc := range []struct { percent int state string }{{100, "ok"}, {11, "ok"}, {10, "low"}, {1, "low"}, {0, "low"}} { lib := &fakeLibrary{usage: pgstore.Usage{RemainingPercent: tc.percent, Spendable: true}} got := decode(t, call(t, v0Server(t, lib, &fakeRuns{}), "GET", "/v0/usage", "")) if got["state"] != tc.state || got["remaining_percent"] != float64(tc.percent) { t.Errorf("%d%%: %v, want state %q", tc.percent, got, tc.state) } // ⚠ `halt_reason` and not `paused_reason`: the ACCOUNT's own vocabulary, typed apart from the // run's since 0.3.0 — a run stops for reasons that say nothing about the account, and lighting // an account-wide state from one would tell a user with money that they have none. if v, ok := got["halt_reason"]; !ok || v != nil { t.Errorf("%d%%: halt_reason = %v (present %v)", tc.percent, v, ok) } if _, ok := got["paused_reason"]; ok { t.Error("usage carries the RUN's vocabulary") } // No window, no resets_at, no sums (contract §Usage). for _, forbidden := range []string{"resets_at", "windows", "period", "usd", "amount"} { if _, ok := got[forbidden]; ok { t.Errorf("usage carries %q", forbidden) } } } } // EVERY contract route sits behind the session guard, and an anonymous caller meets 401 before 404: // the shape of the surface is not public information. // // Driven from the route table rather than a literal list, which is the point: the enumeration was a // literal and stopped naming half the surface the moment routes were added to it. func TestEveryContractRouteRequiresASession(t *testing.T) { deps := Deps{ Log: slog.New(slog.NewJSONHandler(&bytes.Buffer{}, nil)), Auth: &auth.Authenticator{ Sessions: deadSessions{}, IdleTTL: time.Hour, Deny: ProblemHandler(CodeUnauthenticated), }, Library: &fakeLibrary{}, Runs: &fakeRuns{}, Intake: &fakeIntake{}, Upload: UploadLimits{MaxBytes: 1 << 20, Deadline: time.Minute}, } h, err := New(deps) if err != nil { t.Fatal(err) } // ⚠ THE FLOOR MOVED 15 → 14, and it is written down rather than quietly lowered: this gate did // its job — it caught the removal of `POST /books/{bookId}/bank/decisions`, which went with the // per-term signing model D39.144 abolished (owner's word, 22.08). A floor lowered to match a // deliberate, ratified shrink is not a gate fitted to green; a floor lowered without one is. if len(contractSurface) < 14 { t.Fatalf("the route table lists %d routes: the surface cannot have shrunk", len(contractSurface)) } for _, r := range contractSurface { // Path parameters filled in with anything: the guard runs before the handler looks at them. path := APIPrefix + strings.NewReplacer("{bookId}", "bk_1", "{chapterId}", "ch_1", "{runId}", "run_1").Replace(r.path) w := call(t, h, r.method, path, "{}") if w.Code != http.StatusUnauthorized { t.Errorf("%s %s answered %d to a caller with no session", r.method, path, w.Code) } } } // The access log names the ROUTE and never the path: a raw path carries book and run ids, which // identify a user's library in an operator's index. func TestTheAccessLogNamesTheRouteOfAContractCall(t *testing.T) { var logs bytes.Buffer h, err := New(Deps{ Log: slog.New(slog.NewJSONHandler(&logs, nil)), Auth: &auth.Authenticator{ Sessions: liveSessions{}, IdleTTL: time.Hour, Deny: ProblemHandler(CodeUnauthenticated), }, Library: &fakeLibrary{book: pgstore.Book{ID: "bk_secret"}}, Runs: &fakeRuns{}, }) if err != nil { t.Fatal(err) } call(t, h, "GET", "/v0/books/bk_secret", "") if !strings.Contains(logs.String(), `"route":"GET /v0/books/{bookId}"`) { t.Errorf("the access log does not name the route: %s", logs.String()) } if strings.Contains(logs.String(), "bk_secret") { t.Errorf("a book id reached the log: %s", logs.String()) } } // A cursor the server cannot use is its own duty to reject (canon §NextCursor) — and `limit` is the // opposite case, written out because the two look alike and the contract treats them as opposites: // "a deployment that validates this parameter against the schema has to exempt it from rejection" // (§Limit). A page size is a HINT; a cursor is a position, and a wrong one would answer somebody // else's window. // // Mutation caught: re-introducing a 400 on a limit of any shape. func TestABadCursorIsRefusedAndABadLimitIsNot(t *testing.T) { h := v0Server(t, &fakeLibrary{err: pgstore.ErrBadCursor}, &fakeRuns{}) w := call(t, h, "GET", "/v0/books?cursor=nonsense", "") if w.Code != http.StatusBadRequest { t.Errorf("a stale cursor answered %d", w.Code) } if got := decode(t, w); got["code"] != "invalid_request" { t.Errorf("a stale cursor: %v", got) } ok := v0Server(t, &fakeLibrary{}, &fakeRuns{}) for _, q := range []string{"limit=0", "limit=-1", "limit=abc", "limit=100000"} { if w := call(t, ok, "GET", "/v0/books?"+q, ""); w.Code != http.StatusOK { t.Errorf("%s answered %d, want the page it could serve", q, w.Code) } } } // Without a database there is no library to serve, and the prefix stays a guarded 404 rather than a // half-mounted surface. func TestWithoutAReadModelTheVersionedPrefixIsAGuarded404(t *testing.T) { h, err := New(Deps{ Log: slog.New(slog.NewJSONHandler(&bytes.Buffer{}, nil)), Auth: &auth.Authenticator{ Sessions: liveSessions{}, IdleTTL: time.Hour, Deny: ProblemHandler(CodeUnauthenticated), }, }) if err != nil { t.Fatal(err) } if w := call(t, h, "GET", "/v0/books", ""); w.Code != http.StatusNotFound { t.Errorf("status %d, want 404", w.Code) } } // A volume below the schema's own minimum is a MALFORMED request, and 409 is defined to mean the // options moved between the run-options read and this call — so a client answered 409 re-reads // run-options and retries a request that can never succeed. func TestAVolumeBelowTheSchemaMinimumIsARejectedRequestAndNotAMovedBound(t *testing.T) { rn := &fakeRuns{run: pgstore.Run{ID: "run_1"}} h := v0Server(t, &fakeLibrary{}, rn) for _, body := range []string{ `{"stop_for_signing":false,"chapters":0}`, `{"stop_for_signing":false,"chapters":-3}`, `{"stop_for_signing":false,"characters":0}`, `{"stop_for_signing":false,"characters":-1}`, } { w := call(t, h, "POST", "/v0/books/bk_1/runs", body) if w.Code != http.StatusBadRequest { t.Errorf("%s answered %d, want 400", body, w.Code) } } if rn.got.Chapters != nil || rn.got.Characters != nil { t.Errorf("an illegal volume reached the service: %+v", rn.got) } } // RunRequest does not close the object, and inside 0.x a minor bump is where optional fields appear: // a server that refused the whole request would break a client generated against a later 0.x for a // field it was free to ignore. func TestAnUnknownRequestPropertyIsIgnoredRatherThanRefused(t *testing.T) { rn := &fakeRuns{run: pgstore.Run{ID: "run_1", Status: "translating", OrderedChapters: ptr(10)}} w := call(t, v0Server(t, &fakeLibrary{}, rn), "POST", "/v0/books/bk_1/runs", `{"stop_for_signing":true,"chapters":10,"a_field_from_a_later_minor":"x"}`) if w.Code != http.StatusAccepted { t.Fatalf("status %d: %s", w.Code, w.Body) } if rn.got.Chapters == nil || *rn.got.Chapters != 10 || !rn.got.VerifyBank { t.Errorf("the known fields did not survive: %+v", rn.got) } } // ONE counter per book. Reading the card's revision off the RUN row made it lag: a unit_done bumps // the book and its chapter and not the run, so a client that had applied stream frame id=2 got 0 // back and — obeying the contract — dropped the read. // ONE counter per book on the wire. ⚠ The RULE moved in P5 and this test moved with it: the store now // answers a run's revision from its book on every path that hands one out — the card here and the // stop/resume handles, which have no book to override from — so the pin on the rule itself lives in // `pgstore.TestEveryRunTheStoreHandsOutCarriesItsBooksRevision` and `runs.TestEveryRunCarryingAnswerUsesTheBooksRevision`. // What THIS layer owes is that it projects the number it was given and invents nothing, which is // what a fake store can prove and the rule itself no longer is. func TestTheCardProjectsTheRevisionTheStoreGaveAndInventsNone(t *testing.T) { // The two numbers are DIFFERENT on purpose: equal ones cannot tell "projected what it was given" // from "overrode it with the book's", which is exactly the discriminating power an earlier // revision of this test lost when the fixture was levelled (found by acceptance). lib := &fakeLibrary{ book: pgstore.Book{ID: "bk_1", Revision: 41}, run: &pgstore.Run{ID: "run_1", Status: "translating", Revision: 12}, } got := decode(t, call(t, v0Server(t, lib, &fakeRuns{}), "GET", "/v0/books/bk_1", "")) if got["revision"] != float64(41) { t.Errorf("BookDetail.revision = %v, want the book's 41", got["revision"]) } run, _ := got["run"].(map[string]any) if run["revision"] != float64(12) { t.Errorf("Run.revision = %v, want the 12 the store handed over: this layer projects, it does not decide", run["revision"]) } } // "Exhausted" is a fact about the balance, not about the rounded share: $9 left of a $1000 grant // floors to 0% while a run can still be started, and the account screen must not contradict the run // dialog. func TestASmallRemainderIsLowAndNotExhausted(t *testing.T) { lib := &fakeLibrary{usage: pgstore.Usage{RemainingPercent: 0, Spendable: true}} got := decode(t, call(t, v0Server(t, lib, &fakeRuns{}), "GET", "/v0/usage", "")) if got["state"] != "low" { t.Errorf("state %v with money still spendable, want low", got["state"]) } lib.usage = pgstore.Usage{RemainingPercent: 0, Spendable: false} got = decode(t, call(t, v0Server(t, lib, &fakeRuns{}), "GET", "/v0/usage", "")) if got["state"] != "exhausted" { t.Errorf("state %v with nothing left, want exhausted", got["state"]) } } // An instance with a read model and no runner is a read REPLICA, not a broken one: the library is // not run machinery. It used to answer 404 to the library it was holding, while its own boot line // said it was serving one. func TestAnInstanceWithoutARunnerStillServesTheLibrary(t *testing.T) { h, err := New(Deps{ Log: slog.New(slog.NewJSONHandler(&bytes.Buffer{}, nil)), Auth: &auth.Authenticator{ Sessions: liveSessions{}, IdleTTL: time.Hour, Deny: ProblemHandler(CodeUnauthenticated), }, Library: &fakeLibrary{lib: pgstore.Library{Books: []pgstore.Book{{ID: "bk_1"}}}}, }) if err != nil { t.Fatal(err) } for _, path := range []string{"/v0/books", "/v0/books/bk_1", "/v0/usage"} { if w := call(t, h, "GET", path, ""); w.Code != http.StatusOK { t.Errorf("%s answered %d on a read-only instance, want 200", path, w.Code) } } // The run surface stays a guarded 404 rather than a handler that would fail on every call. if w := call(t, h, "GET", "/v0/books/bk_1/run-options", ""); w.Code != http.StatusNotFound { t.Errorf("run-options answered %d with no run lifecycle, want 404", w.Code) } if w := call(t, h, "POST", "/v0/books/bk_1/runs", `{"stop_for_signing":false,"chapters":1}`); w.Code != http.StatusNotFound { t.Errorf("starting a run answered %d with no run lifecycle, want 404", w.Code) } } // The CSRF layer covers the CONTRACT surface, not just /auth/. Without it a cross-site page could // start a paid run with the browser's ambient cookie — the guard that pins the session check does // not see this, because a session is present in both cases. func TestACrossSiteRequestCannotStartARun(t *testing.T) { rn := &fakeRuns{run: pgstore.Run{ID: "run_1"}} h := v0Server(t, &fakeLibrary{}, rn) r := httptest.NewRequest("POST", "/v0/books/bk_1/runs", strings.NewReader(`{"stop_for_signing":false,"chapters":10}`)) r.Header.Set("Content-Type", "application/json") r.AddCookie(&http.Cookie{Name: "__Host-tm_session", Value: "token"}) r.Header.Set("Sec-Fetch-Site", "cross-site") r.Header.Set("Origin", "https://evil.example") w := httptest.NewRecorder() h.ServeHTTP(w, r) if w.Code != http.StatusForbidden { t.Fatalf("a cross-site run start answered %d, want 403", w.Code) } if rn.got.Chapters != nil { t.Errorf("the cross-site request reached the service: %+v", rn.got) } // A same-origin browser request without the client header is refused for the same reason. r = httptest.NewRequest("POST", "/v0/books/bk_1/runs", strings.NewReader(`{"stop_for_signing":false,"chapters":10}`)) r.Header.Set("Content-Type", "application/json") r.AddCookie(&http.Cookie{Name: "__Host-tm_session", Value: "token"}) w = httptest.NewRecorder() h.ServeHTTP(w, r) if w.Code == http.StatusAccepted { t.Error("a cookie-borne POST without X-TM-Client started a run") } } // A route mounted without its guard must fail CLOSED, and loudly: answering 401 would hide the // wiring defect, and serving an anonymous caller as user "" would give them somebody's library. func TestAHandlerWithNoPrincipalFailsClosed(t *testing.T) { var logs bytes.Buffer h := &v0{lib: &fakeLibrary{}, runs: &fakeRuns{}, log: slog.New(slog.NewJSONHandler(&logs, nil))} w := httptest.NewRecorder() h.listBooks(w, httptest.NewRequest("GET", "/v0/books", nil)) if w.Code != http.StatusInternalServerError { t.Errorf("a handler with no principal answered %d, want 500", w.Code) } } // Neither title nor detail may carry engine or database text (contract §Problem): what a client puts // on a screen is whatever this says. func TestAnInternalErrorNeverPutsItsTextOnTheWire(t *testing.T) { secret := "pgstore: relation \"books\" does not exist at character 42" h := v0Server(t, &fakeLibrary{err: errors.New(secret)}, &fakeRuns{}) w := call(t, h, "GET", "/v0/books", "") if w.Code != http.StatusInternalServerError { t.Fatalf("status %d", w.Code) } if strings.Contains(w.Body.String(), "pgstore") || strings.Contains(w.Body.String(), "relation") { t.Errorf("the internal message reached the wire: %s", w.Body) } } // The wire's PausedReason vocabulary is the CONTRACT's, not the platform's. The platform now tells a // halt on its own ceiling from one on the engine's daily limit (pgstore.CeilingPause), and the // contract has a word for the first and none for the second — so the second travels as null, which // is the state the contract itself asks a client to render for a reason it does not know. // // Found by a live probe, not by reading: on the stand a run stopped by the engine's daily ceiling put // `daily_ceiling` on the wire, which a client generated against the spec's `enum: [credit_exhausted]` // would refuse. Register row PD-199 carries the question to the contract's owner. // // Mutation caught: projecting r.PausedReason directly. func TestOnlyTheContractsOwnPausedReasonReachesTheWire(t *testing.T) { for reason, want := range map[string]*string{ pgstore.PausedCreditExhausted: ptr(pgstore.PausedCreditExhausted), pgstore.PausedDailyCeiling: nil, "": nil, "something_later": nil, } { got := projectRun(pgstore.Run{PausedReason: reason}).PausedReason switch { case want == nil && got != nil: t.Errorf("paused_reason %q reached the wire as %q; the contract enumerates one value", reason, *got) case want != nil && (got == nil || *got != *want): t.Errorf("paused_reason %q projected as %v, want %q", reason, got, *want) } } } func ptr[T any](v T) *T { return &v } // The 0.7.0 re-pass purchase on the wire (canon §RunRequest): `re_pass` without a chapter limit // reaches the service as the re-pass; the two members together are 400 (mutually exclusive); and // «nothing to re-pass» answers 409 with its own cause word. func TestARePassRequestIsItsOwnPurchaseShape(t *testing.T) { f := &fakeRuns{run: pgstore.Run{ID: "run_1", Status: "translating"}} h := v0Server(t, &fakeLibrary{}, f) if w := call(t, h, "POST", "/v0/books/bk_1/runs", `{"stop_for_signing":false,"re_pass":true}`); w.Code != http.StatusAccepted { t.Fatalf("a re-pass request answered %d: %s", w.Code, w.Body) } if !f.got.RePass || f.got.Chapters != nil || f.got.Characters != nil { t.Fatalf("the service saw %+v, want RePass with no volume", f.got) } if w := call(t, h, "POST", "/v0/books/bk_1/runs", `{"stop_for_signing":false,"re_pass":true,"chapters":3}`); w.Code != http.StatusBadRequest { t.Fatalf("both purchases at once answered %d, want 400", w.Code) } f.err = runs.ErrRePassUnavailable w := call(t, h, "POST", "/v0/books/bk_1/runs", `{"stop_for_signing":false,"re_pass":true}`) if w.Code != http.StatusConflict { t.Fatalf("nothing-to-re-pass answered %d, want 409", w.Code) } if body := w.Body.String(); !strings.Contains(body, `"re_pass_unavailable"`) { t.Fatalf("the 409 does not carry its own cause: %s", body) } } // ⛔ THE NUMBER SIGNED «CHARACTERS» USED TO LIE, and the fix is a flag BESIDE it rather than a // changed meaning for `null` (unified backlog row 282, form ratified D39.201 §5б). // // The intake counts the non-continuation bytes of the WRITE STREAM. For a UTF-8 text source that IS // the character count, exactly; for an EPUB — a ZIP archive — it counts the runes of COMPRESSED // DATA, which is a property of the container and not reliably even the same order of magnitude. The // engine's manifest now carries the real figure and it is preferred whenever it exists. // // `null` keeps its own meaning: the book is still arriving. Re-using it for «we do not trust this // number» would destroy a meaning the contract already promises to a client. func TestTheCharacterCountSaysWhichNumberItIs(t *testing.T) { engine := int64(48_000) for _, tc := range []struct { name string book pgstore.Book count any exact bool absent bool }{ {name: "the engine has counted the text", book: pgstore.Book{ID: "bk_1", Status: "not_started", CharacterCount: 9_000_000, SourceChars: &engine}, count: float64(48_000), exact: true}, {name: "no manifest read yet: the intake's approximation, labelled", book: pgstore.Book{ID: "bk_1", Status: "not_started", CharacterCount: 9_000_000}, count: float64(9_000_000), exact: false}, {name: "still arriving: null, and the flag says nothing is exact", book: pgstore.Book{ID: "bk_1", Status: "uploading", CharacterCount: 0}, absent: true}, } { t.Run(tc.name, func(t *testing.T) { got := decode(t, call(t, v0Server(t, &fakeLibrary{book: tc.book}, &fakeRuns{}), "GET", "/v0/books/bk_1", "")) book, _ := got["book"].(map[string]any) v, present := book["character_count"] if !present { t.Fatal("character_count is REQUIRED and must never be absent") } switch { case tc.absent && v != nil: t.Errorf("a book still arriving reports %v, want null", v) case !tc.absent && v != tc.count: t.Errorf("character_count = %v, want %v", v, tc.count) } if book["character_count_exact"] != tc.exact { t.Errorf("character_count_exact = %v, want %v", book["character_count_exact"], tc.exact) } }) } } // ⛔ THE CUT'S PROVENANCE ON THE BOOK CARD, and `null` there means «nobody has been asked», which is a // different fact from every word the vocabulary carries. // // It used to be asserted inside the character-count test, as a tenant with no name of its own — and a // contract that cites a pin by name cannot cite one named for something else. Moved rather than // copied: the assertions are the same, the subject is now findable. // // ⚠ THE BOOK HERE IS `not_started`, WHICH IS THE POINT. It has fully arrived and still answers `null`, // because no manifest has been read — so «null while the book is still arriving» is NARROWER than the // fact and a client that waits for arrival waits for the wrong thing. And `null` is not `none`: `none` // is a book the engine looked at and found one chapter in, which is a statement ABOUT the book; // `null` is the absence of any statement. func TestABooksCutProvenanceIsTheEnginesOwnWordOrAnExplicitNull(t *testing.T) { got := decode(t, call(t, v0Server(t, &fakeLibrary{book: pgstore.Book{ID: "bk_1", Status: "not_started"}}, &fakeRuns{}), "GET", "/v0/books/bk_1", "")) book, _ := got["book"].(map[string]any) if v, ok := book["structure"]; !ok || v != nil { t.Errorf("structure = %v (present %v), want an explicit null", v, ok) } got = decode(t, call(t, v0Server(t, &fakeLibrary{book: pgstore.Book{ID: "bk_1", Status: "not_started", Structure: "declared"}}, &fakeRuns{}), "GET", "/v0/books/bk_1", "")) book, _ = got["book"].(map[string]any) if book["structure"] != "declared" { t.Errorf("structure = %v, want the engine's own word", book["structure"]) } // An unknown word travels VERBATIM too: the vocabulary grows additively, and a build that // re-wrote what it did not recognise would hide the growth from the client that must degrade on it. got = decode(t, call(t, v0Server(t, &fakeLibrary{book: pgstore.Book{ID: "bk_1", Status: "not_started", Structure: "delimited"}}, &fakeRuns{}), "GET", "/v0/books/bk_1", "")) book, _ = got["book"].(map[string]any) if book["structure"] != "delimited" { t.Errorf("structure = %v, want the engine's word passed through untouched", book["structure"]) } } // ⛔ A RETIRED MEMBER IS REFUSED, NOT IGNORED, and the difference is a purchase. // // «Unknown properties are ignored» (the test above) exists so a client generated against a LATER // minor is not broken by a field this build has never heard of. `ceiling_chapters` is the opposite // case: a member THIS minor renamed, whose absence now means the ratified default — the WHOLE BOOK. // A 0.10.0 client sending `{"stop_for_signing":false,"ceiling_chapters":60}` means «sixty chapters», // and to a lenient decoder that request says «no volume named»: the client would be charged a hold // on the entire remainder of the book, silently, with a 202 in its hand. // // So it is 400 with the member named. A minor inside major 0 carries breaking changes by design // (canon §Versioning), and a client that has not moved deserves to be told which field did. func TestAClientStillSendingTheRetiredCeilingIsRefusedRatherThanSoldTheWholeBook(t *testing.T) { rn := &fakeRuns{run: pgstore.Run{ID: "run_1", Status: "translating", StartedAt: time.Unix(0, 0).UTC()}} h := v0Server(t, &fakeLibrary{}, rn) for _, body := range []string{ `{"stop_for_signing":false,"ceiling_chapters":60}`, `{"stop_for_signing":false,"ceiling_chapters":0}`, `{"stop_for_signing":false,"chapters":3,"ceiling_chapters":60}`, } { w := call(t, h, "POST", "/v0/books/bk_1/runs", body) if w.Code != http.StatusBadRequest { t.Errorf("%s answered %d, want 400", body, w.Code) } if !strings.Contains(w.Body.String(), "/ceiling_chapters") { t.Errorf("%s: the refusal does not name the member that moved: %s", body, w.Body) } } if rn.got.BookID != "" { t.Errorf("a request naming the retired member reached the service: %+v", rn.got) } // ⚠ The neighbouring rule is UNTOUCHED: a field from a LATER minor is still ignored, and the // difference between the two is the whole point of this test standing beside that one. if w := call(t, h, "POST", "/v0/books/bk_1/runs", `{"stop_for_signing":true,"chapters":3,"a_field_from_a_later_minor":"x"}`); w.Code != http.StatusAccepted { t.Fatalf("an unknown field from a later minor was refused: %d %s", w.Code, w.Body) } } // The two refusals this minor adds, on the wire, with their own cause words — because a client acts // on the CAUSE and the two remedies are opposite: `not_priced` clears by waiting (the engine has not // been asked about this book yet), and re-reading run-options in a loop is exactly the wrong thing; // `chapter_orders_unavailable` never clears by waiting and is answered by ordering in characters. // // Mutation caught: mapping either to CauseBoundsMoved, which tells a client to retry. func TestTheTwoNewRefusalsCarryTheirOwnCauseAndNotBoundsMoved(t *testing.T) { for _, tc := range []struct { err error cause string }{ {runs.ErrNotPriced, `"not_priced"`}, {runs.ErrChapterOrdersUnavailable, `"chapter_orders_unavailable"`}, } { h := v0Server(t, &fakeLibrary{}, &fakeRuns{err: tc.err}) w := call(t, h, "POST", "/v0/books/bk_1/runs", `{"stop_for_signing":false,"chapters":3}`) if w.Code != http.StatusConflict { t.Errorf("%v answered %d, want 409", tc.err, w.Code) } if body := w.Body.String(); !strings.Contains(body, tc.cause) { t.Errorf("%v: the 409 does not carry %s: %s", tc.err, tc.cause, body) } if strings.Contains(w.Body.String(), `"bounds_moved"`) { t.Errorf("%v was answered `bounds_moved`, which tells a client to re-read and retry", tc.err) } // The order form refuses the same way, and that matters more than the start: it is the read a // client polls, and an unpriced book must not look like a priced one with nothing to sell. if w := call(t, h, "GET", "/v0/books/bk_1/run-options", ""); w.Code != http.StatusConflict { t.Errorf("%v on run-options answered %d, want 409", tc.err, w.Code) } } } // `run_limit_reached` reaches the wire, and `credit_exhausted` keeps its own meaning beside it // (PD-446). The two used to be one word, and the word was the ACCOUNT's: measured 04.09, a run stood // `credit_exhausted` while `/usage` answered `state: ok` with 34% of the money untouched. Both // answers were right; the user read a contradiction and went to top up an account that was full. // // Mutation caught: dropping PausedRunLimitReached from ContractPausedReason, which would send it to // the wire as `null` — the neutral halted state, with no remedy attached. func TestBothPauseReasonsReachTheWireAndKeepTheirOwnMeanings(t *testing.T) { for internal, want := range map[string]any{ "run_limit_reached": "run_limit_reached", "credit_exhausted": "credit_exhausted", // Words this side makes and the contract has no name for still travel as null: inventing a // wire value for a distinction the canon does not describe is not this zone's right (PD-199). "daily_ceiling": nil, "ceiling_unknown": nil, } { lib := &fakeLibrary{book: pgstore.Book{ID: "bk_1"}, run: &pgstore.Run{ID: "run_1", Status: "paused", PausedReason: internal}} got := decode(t, call(t, v0Server(t, lib, &fakeRuns{}), "GET", "/v0/books/bk_1", "")) run, _ := got["run"].(map[string]any) if run["paused_reason"] != want { t.Errorf("paused_reason %q reached the wire as %v, want %v", internal, run["paused_reason"], want) } } } // The order form's field set is CLOSED, and it is asserted rather than assumed for the reason the // deleted CeilingBounds test gave: a member that appears on the wire without passing through the // canon is a member a client starts depending on before anybody ratified it. func TestTheOrderFormCarriesTheseMembersAndNoOthers(t *testing.T) { rn := &fakeRuns{bounds: runs.Options{ Options: pricing.Options{ChaptersLeft: 3, AffordableChapters: 3, Verdict: pricing.VerdictCoversAll}, Structure: "detected", ChapterOrders: true, }} got := decode(t, call(t, v0Server(t, &fakeLibrary{}, rn), "GET", "/v0/books/bk_1/run-options", "")) for _, tc := range []struct { where string obj map[string]any want []string }{ {"run-options", got, []string{"order", "balance_micro_usd", "limit", "blocked"}}, {"order", got["order"].(map[string]any), []string{"chapters_left", "affordable_chapters", "verdict", "chapter_orders", "structure", "source_chars", "term_consistency_funded", "estimate"}}, {"estimate", got["order"].(map[string]any)["estimate"].(map[string]any), []string{"expected_micro_usd", "hold_micro_usd"}}, {"limit", got["limit"].(map[string]any), []string{"min_micro_usd", "max_micro_usd"}}, } { if len(tc.obj) != len(tc.want) { t.Errorf("%s carries %d members and the canon describes %d: %v", tc.where, len(tc.obj), len(tc.want), tc.obj) } for _, k := range tc.want { if _, ok := tc.obj[k]; !ok { t.Errorf("%s is missing the required member %q", tc.where, k) } } } }