textmachine/platform/internal/httpapi/v0_test.go

1222 lines
58 KiB
Go
Raw 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 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
signingStop pgstore.BankSigningStop
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) BankSigningStop(context.Context, string, string) (pgstore.BankSigningStop, error) {
return f.signingStop, 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)
}
}
// The form carries the refusal the DOOR will give — and carries it as `blocked`, because the
// verdict has no room for it and must not grow any.
//
// PD-455: a book with a run of its own going was quoted `covers_all`, since the verdict is computed
// from what is left and what the balance covers and nothing asked whether the book was busy. The
// answer is not a fourth verdict value — `covers_all`/`covers_part`/`covers_none` is a closed enum
// and a fourth member breaks every generated client — but the member the canon already pre-authorised
// for growth, whose own instruction to a client is to tolerate a code it does not know.
//
// ⚠ The money half stays exactly as it was: the balance really does cover this book, and `verdict`
// answers about money.
func TestTheFormCarriesTheRunInFlightThatWillRefuseTheClick(t *testing.T) {
rn := &fakeRuns{bounds: runs.Options{
Options: pricing.Options{ChaptersLeft: 3, AffordableChapters: 3, Verdict: pricing.VerdictCoversAll},
Structure: "detected", ChapterOrders: true,
Refusal: pgstore.ErrRunInFlight,
}}
got := decode(t, call(t, v0Server(t, &fakeLibrary{}, rn), "GET", "/v0/books/bk_1/run-options", ""))
blocked, ok := got["blocked"].(map[string]any)
if !ok {
t.Fatalf("the form says nothing about the run that will refuse the click: %v", got["blocked"])
}
// ⚠ THE LITERAL AND NOT THE CONSTANT. Compared to `CauseRunInFlight` this assertion is
// self-consistent and passes at any value — rename the constant and the whole module stays green,
// which the pack's adversarial pass demonstrated by doing it. The word is the canon's (the enum of
// `Blocked.code`), so it is written out here as a word; the second, independent source for it is
// `gates.TestTheBlockedVocabularyServedIsTheOneTheCanonEnumerates`, which reads the canon itself.
if blocked["code"] != "run_in_flight" {
t.Errorf("blocked.code = %v, want %q", blocked["code"], "run_in_flight")
}
// THIS book, not another one: the place the user can act is its own card, where that run is.
if blocked["book_id"] != "bk_1" {
t.Errorf("blocked.book_id = %v, want the book being asked about", blocked["book_id"])
}
if order, _ := got["order"].(map[string]any); order["verdict"] != "covers_all" {
t.Errorf("the verdict moved to %v because a run is going; it answers about money", order["verdict"])
}
}
// `blocked` carries ONE fact and the two candidates are not equal: a run of this book's own means
// there is no start to offer at all, while another book's hold only makes the scale shorter than the
// account could afford. Told the second while the first is true, a client sends the user off to stop
// somebody else's run and the click still fails.
func TestARunOfThisBooksOwnOutranksAnotherBooksHold(t *testing.T) {
rn := &fakeRuns{bounds: runs.Options{
Options: pricing.Options{ChaptersLeft: 3, AffordableChapters: 1, Verdict: pricing.VerdictCoversPart},
Structure: "detected", ChapterOrders: true,
BlockedBy: "bk_other", Refusal: pgstore.ErrRunInFlight,
}}
got := decode(t, call(t, v0Server(t, &fakeLibrary{}, rn), "GET", "/v0/books/bk_1/run-options", ""))
blocked, _ := got["blocked"].(map[string]any)
if blocked["code"] != "run_in_flight" || blocked["book_id"] != "bk_1" {
t.Errorf("blocked = %v, want this book's own run to be what is named", blocked)
}
}
// The two refusals a vanished book directory produces, on the wire.
//
// They are different answers because the FAULT is different, and the difference is the one PD-192
// cost the intake: a book whose own directory is gone is that book's end and says so with a cause
// that tells the client waiting will not help, while a book whose whole volume is gone is the host's
// business and must not reach the user as a verdict about their book.
func TestAVanishedSourceAndAVanishedVolumeAnswerDifferently(t *testing.T) {
h := v0Server(t, &fakeLibrary{}, &fakeRuns{err: runs.ErrSourceGone})
w := call(t, h, "POST", "/v0/books/bk_1/runs", `{"stop_for_signing":false,"chapters":10}`)
if w.Code != http.StatusConflict {
t.Fatalf("a book whose directory is gone: %d, want 409", w.Code)
}
got := decode(t, w)
if got["code"] != string(CodeBookNotReady) {
t.Errorf("code = %v, want %q", got["code"], CodeBookNotReady)
}
// The literal again, for the reason given above: a cause compared to its own constant is a
// tautology, and this one is what tells a client that waiting is not the remedy.
if cause, _ := got["cause"].(map[string]any); cause["code"] != "source_gone" {
t.Errorf("cause = %v, want %q — without it the client offers waiting, which never helps here",
got["cause"], "source_gone")
}
h = v0Server(t, &fakeLibrary{}, &fakeRuns{err: runs.ErrStorageUnavailable})
w = call(t, h, "POST", "/v0/books/bk_1/runs", `{"stop_for_signing":false,"chapters":10}`)
if w.Code != http.StatusServiceUnavailable {
t.Fatalf("an unmounted books volume: %d, want 503 — it is the deployment's fault, not the book's", w.Code)
}
if got := decode(t, w); got["code"] == string(CodeBookNotReady) {
t.Errorf("the host's fault reached the client as a verdict about the book: %v", got["code"])
}
}
// The SAME absence answered on the resume handle, and answered in that handle's own vocabulary.
//
// A caller of `POST /runs/{runId}/resume` is asking about a RUN, so what it is told is that this run
// cannot be continued — `run_not_resumable` — with the cause that says why waiting will not help.
// `book_not_ready`, which the start handle answers for the same fact, is the word for a book still
// arriving or rejected, and it would send a client to wait for an intake that finished long ago.
//
// ⚠ The pair is asserted together on purpose: one fact, two operations, two words, and the thing
// that makes them right is that each belongs to its own operation's table in the canon.
func TestAResumeOverAGoneSourceIsRefusedInTheResumeHandlesOwnWords(t *testing.T) {
h := v0Server(t, &fakeLibrary{}, &fakeRuns{err: runs.ErrSourceGone})
w := call(t, h, "POST", "/v0/runs/run_1/resume", "")
if w.Code != http.StatusConflict {
t.Fatalf("a resume over a gone source: %d, want 409", w.Code)
}
got := decode(t, w)
if got["code"] != "run_not_resumable" {
t.Errorf("code = %v, want %q — the caller asked about a run, not about an intake", got["code"], "run_not_resumable")
}
if cause, _ := got["cause"].(map[string]any); cause["code"] != "source_gone" {
t.Errorf("cause = %v, want %q", got["cause"], "source_gone")
}
// The deployment's half of the same absence keeps the deployment's own answer here too.
h = v0Server(t, &fakeLibrary{}, &fakeRuns{err: runs.ErrStorageUnavailable})
if w := call(t, h, "POST", "/v0/runs/run_1/resume", ""); w.Code != http.StatusServiceUnavailable {
t.Errorf("a resume over an unmounted volume: %d, want 503", w.Code)
}
}
// 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)
}
}
}
}