package books import ( "bytes" "context" "crypto/rand" "encoding/hex" "encoding/json" "errors" "io" "log/slog" "net/url" "os" "os/exec" "path/filepath" "strconv" "strings" "sync" "testing" "time" "github.com/jackc/pgx/v5" "textmachine/platform/internal/ingest" "textmachine/platform/internal/jobs" "textmachine/platform/internal/pgstore" "textmachine/platform/internal/runner" ) // Intake is a walk with three ends — parsed, rejected, or removed — and every one of them is reached // by a DIFFERENT process from the one that started it. That is what these tests are about: the row, // the directory and the file agree at each step, and no step is reachable only from the request that // began it. func intakeDB(t *testing.T) (*pgstore.Store, context.Context) { t.Helper() admin := os.Getenv("TM_PLATFORM_TEST_DSN") if admin == "" { t.Skip("TM_PLATFORM_TEST_DSN not set: the intake's statuses need a live Postgres") } ctx := t.Context() var suffix [6]byte if _, err := rand.Read(suffix[:]); err != nil { t.Fatal(err) } name := "tm_books_test_" + hex.EncodeToString(suffix[:]) conn, err := pgx.Connect(ctx, admin) if err != nil { t.Fatalf("connect: %v", err) } if _, err := conn.Exec(ctx, "create database "+pgx.Identifier{name}.Sanitize()); err != nil { conn.Close(ctx) t.Skipf("cannot create a scratch database (%v): grant CREATEDB or point the DSN at one", err) } t.Cleanup(func() { c, cancel := context.WithTimeout(context.Background(), 15*time.Second) defer cancel() _, _ = conn.Exec(c, "drop database if exists "+pgx.Identifier{name}.Sanitize()+" with (force)") conn.Close(c) }) u, err := url.Parse(admin) if err != nil { t.Fatal(err) } u.Path = "/" + name if err := pgstore.Migrate(ctx, u.String()); err != nil { t.Fatalf("migrate: %v", err) } s, err := pgstore.Open(ctx, u.String()) if err != nil { t.Fatal(err) } t.Cleanup(s.Close) if _, err := s.Pool().Exec(ctx, `insert into users (id, email) values ('u1','u1@example.org')`); err != nil { t.Fatal(err) } return s, ctx } // fakeEngine stands in for `tmctl manifest`. It models the two answers that lead to different ends — // a manifest, and a process that RAN and refused — because a fake that can only succeed proves // nothing about the branch that rejects a book. type fakeEngine struct { mu sync.Mutex manifest ingest.Manifest err error calls int // onManifest runs INSIDE the call, which is where the deadline the caller granted is visible. onManifest func(ctx context.Context) } func (f *fakeEngine) Manifest(ctx context.Context, _, _ string) (ingest.Manifest, error) { f.mu.Lock() f.calls++ hook := f.onManifest m, err := f.manifest, f.err f.mu.Unlock() if hook != nil { hook(ctx) } return m, err } func (f *fakeEngine) set(m ingest.Manifest, err error) { f.mu.Lock() defer f.mu.Unlock() f.manifest, f.err = m, err } func (f *fakeEngine) called() int { f.mu.Lock() defer f.mu.Unlock() return f.calls } // refusal is what one of the engine's answers looks like from here: a process that RAN and exited // with the code that names why. Produced by running a real command rather than by hand-building an // ExitError, so the type AND the code the classifier keys on are the ones a real answer carries. func refusal(t *testing.T, code int) error { t.Helper() err := exec.CommandContext(t.Context(), "/bin/sh", "-c", "exit "+strconv.Itoa(code)).Run() if err == nil { t.Fatalf("the refusal helper exited 0 for code %d", code) } return err } type fixture struct { svc *Service store *pgstore.Store engine *fakeEngine ctx context.Context root string now time.Time } // wholeManifest is a manifest that DESCRIBES ITSELF: every count agrees with what the document // actually carries, and every chapter and pair has an identity. // // ⚠ It exists because the intake's floor became symmetric with the materialiser's (register row // PD-367), and the fixtures were EXTENDED to satisfy it rather than the floor relaxed to admit them. // The old fixture declared 500 chapters and carried none — the very document the materialiser has // always refused and the intake accepted, founding a book with `chapter_count = 500` and an empty // tree over which a run could be started and PAID FOR. A fixture that models an impossible document // proves nothing about the possible ones. func wholeManifest(chapters, unitsPerChapter int) ingest.Manifest { m := ingest.Manifest{ Version: "tm-manifest-v2", ChaptersTotal: chapters, UnitsTotal: chapters * unitsPerChapter, SourceSHA256: strings.Repeat("ab", 32), ChunkerVersion: "chunk-2026.07", } for i := 1; i <= chapters; i++ { c := ingest.ManifestChapter{ID: "c" + strconv.Itoa(i), Number: i, UnitsTotal: unitsPerChapter} for u := range unitsPerChapter { c.Units = append(c.Units, ingest.ManifestUnit{ ID: "c" + strconv.Itoa(i) + ":cut:" + strconv.Itoa(u), FirstChunkIdx: u}) } m.Chapters = append(m.Chapters, c) } return m } func newFixture(t *testing.T) *fixture { t.Helper() store, ctx := intakeDB(t) root := t.TempDir() eng := &fakeEngine{manifest: wholeManifest(500, 2)} now := time.Now().UTC().Truncate(time.Millisecond) f := &fixture{store: store, engine: eng, ctx: ctx, root: root, now: now} f.svc = &Service{ Store: store, Engine: eng, // A deployment declares what it can translate, and the boot now refuses to mount an intake // without it — so the fixture declares it too, or it would model a configuration that cannot // exist. Cfg: Config{BooksDir: root, EngineBinary: "/opt/engine/2026.08.01/tmctl", Pairs: []Pair{{Source: "zh", Target: "ru"}}}, Now: func() time.Time { return f.now }, } return f } // An undeclared deployment refuses every upload rather than accepting it, and a declared pair is // matched case-insensitively — a language tag is case-insensitive by BCP 47, so `zh-Hans` declared // and `zh-hans` uploaded are the same pair. // // Mutation caught: the `len(Pairs) == 0 → true` escape hatch; comparing tags byte-for-byte. func TestAnUndeclaredDeploymentRefusesAndADeclaredPairIgnoresCase(t *testing.T) { f := newFixture(t) f.svc.Cfg.Pairs = nil if _, err := f.svc.Accept(f.ctx, Intake{UserID: "u1", SourceLang: "zh", TargetLang: "ru", Filename: "a.txt", File: strings.NewReader("第一节\n")}); !errors.Is(err, ErrUnsupportedPair) { t.Errorf("a deployment that declares nothing accepted a book: %v", err) } // ⚠ Only the SUBTAG varies: the canon's LangCode fixes the primary subtag as lower-case // (`^[a-z]{2,3}(-[A-Za-z0-9]{2,8})*$`), so `RU` is malformed and refused a step earlier. f.svc.Cfg.Pairs = []Pair{{Source: "zh-Hans", Target: "ru"}} if _, err := f.svc.Accept(f.ctx, Intake{UserID: "u1", SourceLang: "zh-hans", TargetLang: "ru", Filename: "b.txt", File: strings.NewReader("第一节\n")}); err != nil { t.Errorf("a case variant of a declared tag was refused: %v", err) } } // fakeReader records which books were asked for a reading surface. type fakeReader struct { refreshed []pgstore.OwedBook // cuts is the manifest each RefreshCut was HANDED, which is how a test tells "the intake passed // the cut it already had" from "the materializer read it again". cuts []ingest.Manifest claims []pgstore.OwedBook // unclaimable models a debt the materializer's own sweep is already paying. unclaimable bool err error } func (f *fakeReader) Claim(_ context.Context, b pgstore.OwedBook) (pgstore.OwedBook, bool, error) { f.claims = append(f.claims, b) return b, !f.unclaimable, nil } func (f *fakeReader) RefreshCut(_ context.Context, b pgstore.OwedBook, cut ingest.Manifest) error { f.cuts = append(f.cuts, cut) f.refreshed = append(f.refreshed, b) return f.err } // A parse leaves the book OWING a reading surface, recorded where the process that failed to // materialize it cannot lose it. The materialization at the end of the intake is the one with no // other retry: a failure there left the reader screen empty until some later run happened to finish // (PD-276), and the queue that answered it lived in memory. // // Mutation caught: stamping the debt anywhere but the transaction that ends the parse. func TestAParsedBookOwesAReadingSurfaceUntilOneIsMaterialized(t *testing.T) { f := newFixture(t) reader := &fakeReader{err: errors.New("the engine refused")} f.svc.Reader = reader b := f.accept(t, "roman.txt", "第一节\n\n第二节\n") f.provision(t, b) // Before the parse the book is being cut and owes nothing: a debt found here would be one this // test cannot attribute to the parse. if owed := f.owed(t); len(owed) != 0 { t.Fatalf("a book still in intake already owes a surface: %+v", owed) } if err := f.svc.Parse(f.ctx, b.ID); err != nil { t.Fatal(err) } // The intake handed the materializer the cut it had just read, rather than paying for a second // re-chunk of the same source (PD-248). if len(reader.cuts) != 1 || reader.cuts[0].ChaptersTotal != 500 { t.Fatalf("the intake passed %d cuts to the materializer, want the one it had read", len(reader.cuts)) } owed := f.owed(t) if len(owed) != 1 || owed[0].ID != b.ID { t.Fatalf("the materializer's queue holds %+v, want the book whose tree did not land", owed) } // ⚠ …and stamped BY THE PARSE'S OWN TRANSACTION, which is the property and not the debt's mere // presence: a stamp written by a second statement afterwards leaves the same end state and a // window where a book is parsed and owes nothing, and the debt column is the only retry there is. // `xmin` is the transaction that last wrote the row, so the book and the frame the parse emitted // carry one number exactly when one transaction wrote both. f.sameTransaction(t, b.ID, `select b.xmin::text from books b where b.id = $1`, `select e.xmin::text from book_events e where e.book_id = $1 order by e.position desc limit 1`) // …and it is the same debt the intake tried to pay, so the attempt that failed cannot discharge a // boundary recorded after it. if len(reader.refreshed) != 1 || !reader.refreshed[0].OwedAt.Equal(owed[0].OwedAt) { t.Errorf("the intake was handed %+v, want the debt its own parse recorded", reader.refreshed) } } // sameTransaction asserts that two rows were last written by ONE transaction. It is how a test says // "these two facts land together or not at all" without being able to crash the process between them. func (f *fixture) sameTransaction(t *testing.T, bookID, left, right string) { t.Helper() var a, b string if err := f.store.Pool().QueryRow(f.ctx, left, bookID).Scan(&a); err != nil { t.Fatal(err) } if err := f.store.Pool().QueryRow(f.ctx, right, bookID).Scan(&b); err != nil { t.Fatal(err) } if a == "" || a != b { t.Errorf("the two facts were written by different transactions (%s vs %s)", a, b) } } func (f *fixture) owed(t *testing.T) []pgstore.OwedBook { t.Helper() owed, err := f.store.BooksOwedReadModel(f.ctx, 10) if err != nil { t.Fatal(err) } return owed } // accept runs one upload of the given bytes and returns the book. func (f *fixture) accept(t *testing.T, filename, body string) pgstore.Book { t.Helper() b, err := f.svc.Accept(f.ctx, Intake{UserID: "u1", SourceLang: "zh", TargetLang: "ru", Filename: filename, File: strings.NewReader(body)}) if err != nil { t.Fatal(err) } return b } // provision puts an engine configuration in the book's directory. Who does this in production is the // open question the intake stops at (ErrNotProvisioned); here it stands for the operator. func (f *fixture) provision(t *testing.T, b pgstore.Book) string { t.Helper() dir := filepath.Join(f.root, b.ID) if err := os.WriteFile(filepath.Join(dir, runner.ConfigFile), []byte("book_id: x\n"), 0o600); err != nil { t.Fatal(err) } return dir } func (f *fixture) libraryRevision(t *testing.T) int64 { t.Helper() lib, err := f.store.ListBooks(f.ctx, "u1", 50, "") if err != nil { t.Fatal(err) } return lib.Revision } func (f *fixture) card(t *testing.T, id string) pgstore.Book { t.Helper() b, _, err := f.store.GetBook(f.ctx, "u1", id) if err != nil { t.Fatal(err) } return b } // The whole walk, and the assertion is that each status is written by somebody: the row exists while // the file arrives, the engine turns it into a chapter tree, and the library reads the result exactly // as it reads a book the dev intake put there. func TestAnUploadedBookWalksFromUploadingToNotStarted(t *testing.T) { f := newFixture(t) book := f.accept(t, "蛊真人.txt", "первая глава\fвторая глава") if book.Status != "parsing" { t.Fatalf("the response says %q; the file is in, so the book is being parsed", book.Status) } if book.Title != "蛊真人" { t.Fatalf("title %q, want the name the user gave the file", book.Title) } // Characters, not bytes: the contract's figure is a size in characters and this source is Cyrillic. if book.CharacterCount != 25 { t.Fatalf("character_count %d, want 25 runes", book.CharacterCount) } f.provision(t, book) if err := f.svc.Parse(f.ctx, book.ID); err != nil { t.Fatal(err) } got := f.card(t, book.ID) if got.Status != "not_started" || got.ChapterCount != 500 { t.Fatalf("after parsing: %q with %d chapters, want not_started with 500", got.Status, got.ChapterCount) } // The manifest's identity is kept: it says WHICH cut of WHICH bytes produced that chapter count. var sha []byte var chunker string if err := f.store.Pool().QueryRow(f.ctx, `select source_sha256, chunker_version from books where id = $1`, book.ID).Scan(&sha, &chunker); err != nil { t.Fatal(err) } if len(sha) != 32 || chunker != "chunk-2026.07" { t.Fatalf("the manifest's provenance was not recorded: sha %d bytes, chunker %q", len(sha), chunker) } } // `uploading` is a state something can observe, and that is the whole reason the row is written // before the body is read: a second reader of the library sees the book while its file is still on // the wire. func TestABookIsVisibleAsUploadingWhileItsFileIsStillArriving(t *testing.T) { f := newFixture(t) reading := make(chan struct{}) release := make(chan struct{}) body := &blockingReader{data: "the whole book", gate: reading, release: release} done := make(chan error, 1) go func() { _, err := f.svc.Accept(f.ctx, Intake{UserID: "u1", SourceLang: "zh", TargetLang: "ru", Filename: "book.txt", File: body}) done <- err }() <-reading lib, err := f.store.ListBooks(f.ctx, "u1", 10, "") if err != nil { t.Fatal(err) } if len(lib.Books) != 1 || lib.Books[0].Status != "uploading" { t.Fatalf("the library shows %+v while the file is arriving, want one book in uploading", lib.Books) } close(release) if err := <-done; err != nil { t.Fatal(err) } if got := f.card(t, lib.Books[0].ID); got.Status != "parsing" { t.Fatalf("after the body arrived the book is %q, want parsing", got.Status) } } // blockingReader hands over its first chunk and then waits, so a test can look at the world in the // middle of an upload. type blockingReader struct { data string gate chan struct{} release chan struct{} sent bool } func (b *blockingReader) Read(p []byte) (int, error) { if !b.sent { b.sent = true n := copy(p, b.data) close(b.gate) <-b.release return n, nil } return 0, io.EOF } // An upload that did not finish leaves NOTHING: not a row the user cannot delete — the contract has // no handle for that — and not a directory nothing points at. // // The client going AWAY is the ordinary way to get here, so that is what the test does: the request's // own context is cancelled before the body fails. Cleaning up on it would then clean up nothing. func TestAnAbandonedUploadLeavesNeitherARowNorADirectory(t *testing.T) { f := newFixture(t) ctx, cancel := context.WithCancel(f.ctx) defer cancel() _, err := f.svc.Accept(ctx, Intake{UserID: "u1", SourceLang: "zh", TargetLang: "ru", Filename: "book.txt", File: &failingReader{cancel: cancel}}) if err == nil { t.Fatal("an upload whose body failed was accepted") } lib, err := f.store.ListBooks(f.ctx, "u1", 10, "") if err != nil { t.Fatal(err) } if len(lib.Books) != 0 { t.Fatalf("the library holds %+v after a failed upload", lib.Books) } entries, err := os.ReadDir(f.root) if err != nil { t.Fatal(err) } // Everything except the storage marker, which is the root's own and outlives every book: it is // written by the first upload and never removed, because its absence is what tells the intake the // volume is gone (FP5-10). var left []string for _, e := range entries { if e.Name() != StorageMarker { left = append(left, e.Name()) } } if len(left) != 0 { t.Fatalf("the books root holds %v after a failed upload", left) } } // failingReader is a client that goes away mid-body. gate/release, when set, let a test look at the // world while the upload is still in flight. type failingReader struct { cancel context.CancelFunc gate chan struct{} release chan struct{} } func (f failingReader) Read([]byte) (int, error) { if f.gate != nil { close(f.gate) <-f.release } if f.cancel != nil { f.cancel() } return 0, errors.New("the client went away") } // A source the engine READ and found no book in is terminal, and its bytes go: nothing re-parses // them, no path downloads them, and an authenticated route that writes to an operator's disk and // never removes anything is a hole this one must not open. // // ⚠ Exit 11 and no other number. This is the ONLY code that reaches the destructive end of the walk // (PD-196); the tests below hold the other side of that line. func TestASourceTheEngineRefusesIsRejectedAndItsFileRemoved(t *testing.T) { f := newFixture(t) book := f.accept(t, "not-a-book.pdf", "%PDF-1.7 …") dir := f.provision(t, book) f.engine.set(ingest.Manifest{}, refusal(t, ingest.ExitSourceUnreadable)) // ⚠ The FIRST refusal is still not terminal, and that is now a deliberate margin rather than a // necessity: the engine's verdict is unambiguous, and the budget is kept because it costs an // empty book five $0 calls and costs a mistaken verdict nothing (see parseAttempts). if err := f.svc.Parse(f.ctx, book.ID); err != nil { t.Fatal(err) } if got := f.card(t, book.ID); got.Status != "parsing" { t.Fatalf("one refusal made the book %q; a single exit 1 must not be terminal", got.Status) } if _, err := os.Stat(filepath.Join(dir, SourceName+".pdf")); err != nil { t.Fatalf("the source was deleted on the first refusal: %v", err) } for range parseAttempts { f.now = f.now.Add(claimGrace + time.Minute) if err := f.svc.Parse(f.ctx, book.ID); err != nil { t.Fatal(err) } } got := f.card(t, book.ID) if got.Status != "rejected" { t.Fatalf("a source the engine refused is %q, want rejected", got.Status) } if _, err := os.Stat(dir); !errors.Is(err, os.ErrNotExist) { t.Fatalf("the rejected book's directory is still there: %v", err) } // The reason is the platform's own word and is kept for an operator — contract v0 has no field // for it, so it is deliberately not on the wire. var reason string if err := f.store.Pool().QueryRow(f.ctx, `select reject_reason from books where id = $1`, book.ID).Scan(&reason); err != nil { t.Fatal(err) } if reason != ReasonSourceUnreadable { t.Fatalf("reject_reason %q, want %q", reason, ReasonSourceUnreadable) } } // The other side of that line, and it is the whole of PD-196: every answer of the engine EXCEPT // exit 11 leaves the user's upload on disk. // // Before the refusal band the engine had one number for all of them, so an operator's typo in a // book.yaml, a lock held by their own tmctl and a source that genuinely is not a book arrived here // as the same fact — and the only verdict this side could give it deleted the file. Each case below // is a deployment or host condition, and none of them is the user's to pay for. // // Mutation caught: mapping anything but ingest.ExitSourceUnreadable onto ReasonSourceUnreadable. func TestOnlyTheOneRefusalClassAboutTheTextEverCostsTheUpload(t *testing.T) { cases := []struct { name string code int // terminal says whether the budget can ever run out for this answer. Two of them never spend // an attempt at all, so the book waits for a human instead of being rejected. terminal bool want string }{ {"the configuration will not load", ingest.ExitConfigInvalid, false, ReasonNotConfigured}, {"another process holds the project", ingest.ExitProjectLocked, true, ReasonParserUnavailable}, {"a refusal class this build has no name for", ingest.ExitRefusedOther, true, ReasonParserUnavailable}, {"an ordinary unclassified failure", 1, true, ReasonParserUnavailable}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { f := newFixture(t) book := f.accept(t, "book.txt", "первая глава") dir := f.provision(t, book) f.engine.set(ingest.Manifest{}, refusal(t, tc.code)) for range parseAttempts + 1 { f.now = f.now.Add(claimGrace + time.Minute) if err := f.svc.Parse(f.ctx, book.ID); err != nil { t.Fatal(err) } } got := f.card(t, book.ID) switch { case tc.terminal && got.Status != "rejected": t.Errorf("after %d answers the book is %q, want rejected", parseAttempts+1, got.Status) case !tc.terminal && got.Status != "parsing": t.Errorf("the book is %q; a deployment fault must never end an intake", got.Status) } // The assertion that matters, and it holds for BOTH ends: the bytes the user sent are // still there. if _, err := os.Stat(filepath.Join(dir, SourceName+".txt")); err != nil { t.Fatalf("exit %d cost the user their upload: %v", tc.code, err) } if reason := intakeReason(refusal(t, tc.code)); reason != tc.want { t.Errorf("exit %d classified as %q, want %q", tc.code, reason, tc.want) } }) } } // A verdict about the book is only a verdict when the engine ANSWERED. A process the machine killed, // and a binary that never started, say nothing about anyone's text. func TestAnEngineThatDidNotAnswerIsNeverAVerdictAboutTheBook(t *testing.T) { killed := exec.CommandContext(t.Context(), "/bin/sh", "-c", "kill -TERM $$").Run() if killed == nil { t.Fatal("the helper was not killed") } for _, err := range []error{killed, &exec.Error{Name: "tmctl", Err: os.ErrNotExist}, context.DeadlineExceeded} { if got := intakeReason(err); got != ReasonParserUnavailable { t.Errorf("%v classified as %q, want %q", err, got, ReasonParserUnavailable) } } } // An engine that cannot be RUN is this host's problem and not this book's. The parse is retried — // and bounded, because a book that waits forever is the failure this must not trade for (PD-162). func TestAnEngineThatCannotBeRunIsRetriedAndThenGivenUpOn(t *testing.T) { f := newFixture(t) book := f.accept(t, "book.txt", "text") f.provision(t, book) f.engine.set(ingest.Manifest{}, &exec.Error{Name: "tmctl", Err: os.ErrNotExist}) // ⚠ The clock moves between attempts because the CLAIM is what spaces them: a failed parse keeps // it, so the book is not claimable again until the grace passes. Without that the five attempts // would burn in five ticks of the sweep — 75 seconds — instead of in the time this budget is // meant to bound, and an engine outage of a couple of minutes would reject every book waiting. for i := 1; i < parseAttempts; i++ { if err := f.svc.Parse(f.ctx, book.ID); err != nil { t.Fatal(err) } if got := f.card(t, book.ID); got.Status != "parsing" { t.Fatalf("attempt %d left the book %q, want it still parsing", i, got.Status) } // The very next call, before the grace, must do NOTHING at all. before := f.engine.called() if err := f.svc.Parse(f.ctx, book.ID); err != nil { t.Fatal(err) } if f.engine.called() != before { t.Fatalf("attempt %d was retried before the grace passed", i) } f.now = f.now.Add(claimGrace + time.Minute) } if err := f.svc.Parse(f.ctx, book.ID); err != nil { t.Fatal(err) } got := f.card(t, book.ID) if got.Status != "rejected" { t.Fatalf("after %d attempts the book is %q, want rejected", parseAttempts, got.Status) } // The file STAYS: the fault was the deployment's, and deleting a user's upload because this host // was misconfigured is not a decision to make on their behalf. if _, err := os.Stat(filepath.Join(f.root, book.ID, SourceName+".txt")); err != nil { t.Fatalf("the source of a book rejected for a deployment fault was removed: %v", err) } } // The seam of the open question: a book directory with no engine configuration cannot be parsed, and // the platform does not write that file (D39.110 §2b). // // ⚠ This test asserts the OPPOSITE of what it did before the cross-family review, and the reversal is // deliberate. It used to pin "five attempts, then rejected" — which meant that on any deployment // without an answer to the `book.yaml` question (that is, every deployment today) EVERY upload was // destroyed within the hour, over a gap the user cannot see and an operator can fix in a second. // A missing configuration is a DEPLOYMENT state, so the book waits in `parsing`, visibly, with its // source intact, and one file dropped in is all it takes. func TestABookWithNoEngineConfigurationWaitsRatherThanDies(t *testing.T) { f := newFixture(t) book := f.accept(t, "book.txt", "text") for range parseAttempts * 2 { if err := f.svc.Parse(f.ctx, book.ID); err != nil { t.Fatal(err) } f.now = f.now.Add(claimGrace + time.Minute) // the claim spaces the retries } if f.engine.called() != 0 { t.Fatalf("the engine was asked %d times about a book it has no configuration for", f.engine.called()) } var status, reason string if err := f.store.Pool().QueryRow(f.ctx, `select status, reject_reason from books where id = $1`, book.ID).Scan(&status, &reason); err != nil { t.Fatal(err) } if status != "parsing" || reason != "" { t.Fatalf("a book with no configuration is %q/%q; it must WAIT, not be destroyed", status, reason) } if _, err := os.Stat(filepath.Join(f.root, book.ID, SourceName+".txt")); err != nil { t.Fatalf("the source of a book waiting for its configuration was removed: %v", err) } // And the moment somebody provisions it, the very next pass parses it. f.provision(t, pgstore.Book{ID: book.ID}) f.now = f.now.Add(claimGrace + time.Minute) if err := f.svc.Parse(f.ctx, book.ID); err != nil { t.Fatal(err) } if got := f.card(t, book.ID); got.Status != "not_started" { t.Fatalf("after provisioning the book is %q, want not_started", got.Status) } } // Two parsers on one book would be two writers of the engine's project file. The claim is a // compare-and-set, and losing that race is the ordinary case rather than a failure. func TestOnlyOneOfTwoParsersOfOneBookAsksTheEngine(t *testing.T) { f := newFixture(t) book := f.accept(t, "book.txt", "text") f.provision(t, book) var wg sync.WaitGroup errs := make([]error, 8) for i := range errs { wg.Add(1) go func() { defer wg.Done() errs[i] = f.svc.Parse(f.ctx, book.ID) }() } wg.Wait() for i, err := range errs { if err != nil { t.Errorf("parser %d: %v", i, err) } } if n := f.engine.called(); n != 1 { t.Fatalf("the engine was asked %d times for one book, want exactly 1", n) } } // The backstop: both halves of a stuck intake are reachable only from the sweep, because the process // that could have finished them is the one that died. func TestTheSweepFinishesAWalkNobodyElseCan(t *testing.T) { f := newFixture(t) stale := f.accept(t, "abandoned.txt", "half a file") // This one never got past `uploading`: its request is gone. if _, err := f.store.Pool().Exec(f.ctx, `update books set status = 'uploading' where id = $1`, stale.ID); err != nil { t.Fatal(err) } waiting := f.accept(t, "waiting.txt", "text") f.provision(t, waiting) f.now = f.now.Add(2 * time.Hour) // past both graces if err := f.svc.Sweep(f.ctx); err != nil { t.Fatal(err) } lib, err := f.store.ListBooks(f.ctx, "u1", 10, "") if err != nil { t.Fatal(err) } if len(lib.Books) != 1 || lib.Books[0].ID != waiting.ID { t.Fatalf("the library holds %+v, want only the book that finished uploading", lib.Books) } if got := f.card(t, waiting.ID); got.Status != "not_started" { t.Fatalf("the waiting book is %q, want the sweep to have parsed it", got.Status) } if _, err := os.Stat(filepath.Join(f.root, stale.ID)); !errors.Is(err, os.ErrNotExist) { t.Fatalf("the abandoned upload's directory survived the sweep: %v", err) } } // Adding a book must MOVE the library's revision. The contract asks a client to drop a read whose // revision is not above what it applied, so a book that joins at a number the library already had is // a book the screen that uploaded it never sees (register row PD-122). func TestABookThatJoinsTheLibraryMovesItsRevision(t *testing.T) { f := newFixture(t) before, err := f.store.ListBooks(f.ctx, "u1", 10, "") if err != nil { t.Fatal(err) } first := f.accept(t, "one.txt", "text") after, err := f.store.ListBooks(f.ctx, "u1", 10, "") if err != nil { t.Fatal(err) } if after.Revision <= before.Revision { t.Fatalf("the library revision stayed at %d after a book joined it", after.Revision) } f.provision(t, first) if err := f.svc.Parse(f.ctx, first.ID); err != nil { t.Fatal(err) } parsed, err := f.store.ListBooks(f.ctx, "u1", 10, "") if err != nil { t.Fatal(err) } if parsed.Revision <= after.Revision { t.Fatalf("the library revision stayed at %d when a book's status changed", parsed.Revision) } } // A book the DEV intake registered carries a workdir the operator chose — their own project // directory, with their own source in it. No path here may remove one. func TestADirectoryOutsideTheBooksRootIsNeverRemoved(t *testing.T) { f := newFixture(t) outside := t.TempDir() if err := os.WriteFile(filepath.Join(outside, "irreplaceable.txt"), []byte("x"), 0o600); err != nil { t.Fatal(err) } f.svc.removeDir(outside) if _, err := os.Stat(filepath.Join(outside, "irreplaceable.txt")); err != nil { t.Fatalf("a directory outside the books root was removed: %v", err) } for _, dir := range []string{f.root, filepath.Join(f.root, ".."), "", "/"} { if f.svc.owns(dir) { t.Errorf("owns(%q) is true; only paths BELOW the root are this service's", dir) } } if !f.svc.owns(filepath.Join(f.root, "bk_1")) { t.Error("a book directory under the root is not recognised as this service's") } } // The two things the intake takes from a name it did not write. func TestWhatTheUploadedFileNameIsAllowedToDecide(t *testing.T) { titles := []struct{ in, want string }{ {"蛊真人.epub", "蛊真人"}, {"/etc/passwd", "passwd"}, {"../../secret.txt", "secret"}, {"", ""}, {"no-extension", "no-extension"}, {".hidden", ".hidden"}, {strings.Repeat("x", 400) + ".txt", strings.Repeat("x", MaxTitle)}, {"line\nbreak.txt", "linebreak"}, } for _, tc := range titles { if got := titleFrom(tc.in); got != tc.want { t.Errorf("titleFrom(%q) = %q, want %q", tc.in, got, tc.want) } } exts := []struct{ in, want string }{ {"book.epub", ".epub"}, {"BOOK.EPUB", ".epub"}, {"book.txt", ".txt"}, {"book", ".txt"}, {"book.", ".txt"}, {"book.tar.gz", ".gz"}, {"evil.sh;rm -rf", ".txt"}, {"evil../../x", ".txt"}, {"book.verylongextension", ".txt"}, } for _, tc := range exts { if got := extensionOf(tc.in); got != tc.want { t.Errorf("extensionOf(%q) = %q, want %q", tc.in, got, tc.want) } } } // The languages are CODES and they enter here from a browser. func TestALanguageThatIsNotACodeIsRefused(t *testing.T) { f := newFixture(t) for _, bad := range []string{"", "Chinese", "zh_CN", "ZH", "../zh", "z"} { _, err := f.svc.Accept(f.ctx, Intake{UserID: "u1", SourceLang: bad, TargetLang: "ru", Filename: "b.txt", File: strings.NewReader("x")}) if !errors.Is(err, ErrBadIntake) { t.Errorf("source_lang %q: %v, want ErrBadIntake", bad, err) } } if entries, err := os.ReadDir(f.root); err != nil || len(entries) != 0 { t.Fatalf("a refused intake left %v in the books root (%v)", entries, err) } } // The engine dispatches its reader by EXTENSION (backend/internal/chunk/ingest.go: `.epub` → the // epub reader, anything else → plain text), so the extension has to survive intake or an EPUB is // read as text and the book comes out as one chapter of markup. func TestTheUploadedFilesExtensionSurvivesIntake(t *testing.T) { f := newFixture(t) book := f.accept(t, "蛊真人.EPUB", "PK\x03\x04 not really an epub") if _, err := os.Stat(filepath.Join(f.root, book.ID, SourceName+".epub")); err != nil { t.Fatalf("the source was not written as source.epub: %v", err) } } // The zone's log discipline (ENGINEERING_STANDARDS §Наблюдаемость, PD-3): an INFO line does not // identify a user's library. The intake writes several, so this asserts the whole walk at once. func TestNoBookIdentifierReachesAnInfoLine(t *testing.T) { f := newFixture(t) var buf bytes.Buffer f.svc.Log = slog.New(slog.NewJSONHandler(&buf, &slog.HandlerOptions{Level: slog.LevelInfo})) book := f.accept(t, "book.txt", "text") f.provision(t, book) if err := f.svc.Parse(f.ctx, book.ID); err != nil { t.Fatal(err) } f.now = f.now.Add(2 * time.Hour) if err := f.svc.Sweep(f.ctx); err != nil { t.Fatal(err) } for _, line := range strings.Split(strings.TrimSpace(buf.String()), "\n") { if line == "" { continue } var rec map[string]any if err := json.Unmarshal([]byte(line), &rec); err != nil { t.Fatal(err) } if rec["level"] != "INFO" && rec["level"] != "WARN" { continue // an ERROR may carry the path an operator needs — the open class of PD-139 } if strings.Contains(line, book.ID) { t.Errorf("a book id reached a %s line: %s", rec["level"], line) } } } // The library's revision may never go BACKWARDS, and an abandoned upload is the one deletion this // zone has: the book that joined at the top of the account's scale is removed when its request goes // away, and the maximum would fall with it. A client that had applied the higher number must then, by // the contract, drop every later read of the library — the screen keeps rendering a book that is gone. func TestCancellingAnUploadDoesNotWindTheLibraryBack(t *testing.T) { f := newFixture(t) kept := f.accept(t, "kept.txt", "text") f.provision(t, kept) before, err := f.store.ListBooks(f.ctx, "u1", 10, "") if err != nil { t.Fatal(err) } // The number that matters is the one a CLIENT could have seen — the library WHILE the doomed // upload was in it, not the library before it arrived. Read from inside the upload, which is // exactly where a second tab would read it. reading, release := make(chan struct{}), make(chan struct{}) ctx, cancel := context.WithCancel(f.ctx) defer cancel() done := make(chan error, 1) go func() { _, err := f.svc.Accept(ctx, Intake{UserID: "u1", SourceLang: "zh", TargetLang: "ru", Filename: "abandoned.txt", File: &failingReader{cancel: cancel, gate: reading, release: release}}) done <- err }() <-reading during, err := f.store.ListBooks(f.ctx, "u1", 10, "") if err != nil { t.Fatal(err) } if during.Revision <= before.Revision { t.Fatalf("the upload joined the library at revision %d, which is not above %d", during.Revision, before.Revision) } close(release) if err := <-done; err == nil { t.Fatal("an upload whose body failed was accepted") } after, err := f.store.ListBooks(f.ctx, "u1", 10, "") if err != nil { t.Fatal(err) } if after.Revision < during.Revision { t.Fatalf("the library revision went back from %d to %d when the upload was cancelled", during.Revision, after.Revision) } if len(after.Books) != 1 || after.Books[0].ID != kept.ID { t.Fatalf("the library holds %+v, want only the book that stayed", after.Books) } } // The sweep decides from a snapshot too, and its cure is the same: the directory of a book goes only // when the ROW went with it. A request that finished while the sweep was looking moves the book to // `parsing`, DeleteUpload's status guard then refuses — and removing the directory anyway would // delete the source of a live book, leaving one that can only end rejected and an hour of somebody's // upload gone without a word. func TestTheSweepNeverRemovesTheSourceOfABookItCouldNotDelete(t *testing.T) { f := newFixture(t) book := f.accept(t, "book.txt", "text") // already `parsing`: its upload finished f.svc.abandon(f.ctx, book.ID, filepath.Join(f.root, book.ID)) if got := f.card(t, book.ID); got.Status != "parsing" { t.Fatalf("the row is %q, want the parsing book it became", got.Status) } if _, err := os.Stat(filepath.Join(f.root, book.ID, SourceName+".txt")); err != nil { t.Fatalf("the source of a book that was NOT deleted was removed: %v", err) } } // The two constants are a PAIR and neither package can see the other's reasoning, so the pair is // asserted here. A claim grace shorter than the queue's job timeout means the backstop sweep takes a // parse away from the worker that is still running it: both then meet on one project directory, the // loser dies on the engine's exclusive lock with exit 1 — and exit 1 is how the engine says "I cannot // cut this source". func TestTheClaimGraceOutlivesTheQueuesJobTimeout(t *testing.T) { if claimGrace <= jobs.JobTimeout { t.Fatalf("claimGrace %s does not outlive the queue's job timeout %s: the sweep would steal a claim from a running parse", claimGrace, jobs.JobTimeout) } } // Every byte is in and the client hung up while waiting for the 201 — a phone that went to sleep, a // tab that was closed. The book must still be theirs: the row is what makes it findable at all, and // throwing away a completed upload because nobody is left to read the answer costs the user the whole // file and the whole wait. func TestAnUploadSurvivesAClientThatHangsUpAfterTheLastByte(t *testing.T) { f := newFixture(t) ctx, cancel := context.WithCancel(f.ctx) book, err := f.svc.Accept(ctx, Intake{UserID: "u1", SourceLang: "zh", TargetLang: "ru", Filename: "book.txt", File: &hangUpReader{data: "первая глава", cancel: cancel}}) if err != nil { t.Fatalf("an upload whose client left after the last byte was dropped: %v", err) } if book.Status != "parsing" { t.Fatalf("the book is %q, want parsing", book.Status) } if _, err := os.Stat(filepath.Join(f.root, book.ID, SourceName+".txt")); err != nil { t.Fatalf("the received source is gone: %v", err) } } // hangUpReader delivers the whole body and then cancels the request's context, which is what a client // that walks away after sending everything looks like from here. type hangUpReader struct { data string cancel context.CancelFunc sent bool } func (h *hangUpReader) Read(p []byte) (int, error) { if !h.sent { h.sent = true return copy(p, h.data), nil } h.cancel() return 0, io.EOF } // FP5-1 (acceptance, HIGH). A library revision must identify ONE state of the library. The floor a // cancelled upload raises is only half the rule: if the next book joins from the maximum alone it // lands at or below that floor, and the same number then answers for three different libraries — // before the cancelled upload, during it, and after the next one arrived. func TestABookThatJoinsAfterACancelledUploadStillMovesTheRevision(t *testing.T) { f := newFixture(t) first := f.accept(t, "kept.txt", "text") f.provision(t, first) before := f.libraryRevision(t) ctx, cancel := context.WithCancel(f.ctx) defer cancel() if _, err := f.svc.Accept(ctx, Intake{UserID: "u1", SourceLang: "zh", TargetLang: "ru", Filename: "abandoned.txt", File: &failingReader{cancel: cancel}}); err == nil { t.Fatal("an upload whose body failed was accepted") } afterCancel := f.libraryRevision(t) if afterCancel < before { t.Fatalf("the revision went back after a cancelled upload: %d → %d", before, afterCancel) } next := f.accept(t, "next.txt", "text") afterNext := f.libraryRevision(t) if afterNext <= afterCancel { t.Fatalf("a book that joined after a cancelled upload left the revision at %d (was %d): the same number now answers for two different libraries", afterNext, afterCancel) } // And the book itself carries a revision above the floor, so its own card is not stale on arrival. var rev int64 if err := f.store.Pool().QueryRow(f.ctx, `select revision from books where id = $1`, next.ID).Scan(&rev); err != nil { t.Fatal(err) } if rev <= afterCancel { t.Fatalf("the new book joined at revision %d, at or below the floor %d", rev, afterCancel) } } // FP5-4 (acceptance). Waiting for a configuration must not bring DELETION closer: every claim counted // against the budget, so a book that waited out five graces had its budget spent before the engine // was asked once — and the first answer, which the engine gives as exit 1 for a typo in `book.yaml` // as much as for an unreadable file, was terminal on arrival and took the source with it. func TestWaitingForAConfigurationDoesNotBringDeletionCloser(t *testing.T) { f := newFixture(t) book := f.accept(t, "book.txt", "text") for range parseAttempts * 3 { if err := f.svc.Parse(f.ctx, book.ID); err != nil { t.Fatal(err) } f.now = f.now.Add(claimGrace + time.Minute) } var attempts int if err := f.store.Pool().QueryRow(f.ctx, `select parse_attempts from books where id = $1`, book.ID).Scan(&attempts); err != nil { t.Fatal(err) } if attempts != 0 { t.Fatalf("waiting for a configuration spent %d attempts of the budget", attempts) } // Now it is provisioned and the engine refuses the source once: with the budget intact, that first // answer must NOT be terminal and must NOT delete anything. f.provision(t, pgstore.Book{ID: book.ID}) f.engine.set(ingest.Manifest{}, refusal(t, ingest.ExitSourceUnreadable)) if err := f.svc.Parse(f.ctx, book.ID); err != nil { t.Fatal(err) } if got := f.card(t, book.ID); got.Status != "parsing" { t.Fatalf("the first engine refusal after a long wait made the book %q", got.Status) } if _, err := os.Stat(filepath.Join(f.root, book.ID, SourceName+".txt")); err != nil { t.Fatalf("the source was deleted on the first refusal after a wait: %v", err) } } // FP5-3 (acceptance). The rejection path removes the directory BEFORE it writes the row, so a crash // between the two leaves a book in `parsing` with nothing on disk. That must be terminal — otherwise // the missing directory reads as "not configured", which is the one reason that never ends, and the // book waits for a configuration nobody can put anywhere. func TestABookWhoseDirectoryIsGoneIsRejectedRatherThanLeftWaiting(t *testing.T) { f := newFixture(t) book := f.accept(t, "book.txt", "text") f.provision(t, book) if err := os.RemoveAll(filepath.Join(f.root, book.ID)); err != nil { t.Fatal(err) } if err := f.svc.Parse(f.ctx, book.ID); err != nil { t.Fatal(err) } var status, reason string if err := f.store.Pool().QueryRow(f.ctx, `select status, reject_reason from books where id = $1`, book.ID).Scan(&status, &reason); err != nil { t.Fatal(err) } if status != "rejected" || reason != ReasonSourceUnreadable { t.Fatalf("a book with no directory is %q/%q, want rejected/%s", status, reason, ReasonSourceUnreadable) } if f.engine.called() != 0 { t.Fatalf("the engine was asked %d times about a book with no directory", f.engine.called()) } } // FP5-5 (acceptance): the backstop's re-drive of a parse is the SAME engine call the queue gives // fifteen minutes. Under the run sweep's two-minute pass it was killed by the deadline, the kill was // read as a host that cannot run the engine, and the attempt was spent — every pass, until the book // was rejected for being large. func TestOneBookInTheSweepGetsABudgetAParseCanLiveIn(t *testing.T) { f := newFixture(t) book := f.accept(t, "book.txt", "text") f.provision(t, book) // The engine takes longer than a second and reports the deadline it was given. var granted time.Duration f.engine.onManifest = func(ctx context.Context) { if dl, ok := ctx.Deadline(); ok { granted = time.Until(dl) } } f.now = f.now.Add(2 * time.Hour) // past the graces, so the sweep picks the book up if err := f.svc.Sweep(f.ctx); err != nil { t.Fatal(err) } // Not an equality: the deadline is read a few microseconds after it was granted. What matters is // that a parse gets the same ORDER of time from the sweep as it does from the queue, rather than // the two minutes the run pass runs on. if granted < jobs.JobTimeout-time.Minute { t.Fatalf("the sweep gave one book's parse %s; the same call gets %s from the queue", granted, jobs.JobTimeout) } } // Cross-family review of the acceptance dofix (H1): the storage ROOT being gone — an unmounted // volume, a deployment pointed at a path that is not there yet — looks exactly like one book's // directory being gone, and the two mean opposite things. Read as the second, ONE sweep rejects // every book in intake with "the source cannot be read": a reason that blames the user's file, is // terminal by design, and has no way back. func TestAVanishedStorageRootIsNotEveryBooksFault(t *testing.T) { f := newFixture(t) first := f.accept(t, "one.txt", "text") second := f.accept(t, "two.txt", "text") f.provision(t, first) f.provision(t, second) if err := os.RemoveAll(f.root); err != nil { t.Fatal(err) } // Well past the budget: waiting for a host to come back must never bring a rejection closer. for range parseAttempts * 2 { for _, b := range []pgstore.Book{first, second} { if err := f.svc.Parse(f.ctx, b.ID); err != nil { t.Fatal(err) } } f.now = f.now.Add(claimGrace + time.Minute) } if f.engine.called() != 0 { t.Fatalf("the engine was asked %d times while the storage root was gone", f.engine.called()) } for _, b := range []pgstore.Book{first, second} { var status, reason string var attempts int if err := f.store.Pool().QueryRow(f.ctx, `select status, reject_reason, parse_attempts from books where id = $1`, b.ID). Scan(&status, &reason, &attempts); err != nil { t.Fatal(err) } if status != "parsing" || reason != "" { t.Fatalf("book %s is %q/%q after the storage root vanished; the host is at fault, not the book", b.ID, status, reason) } if attempts != 0 { t.Fatalf("book %s spent %d attempts on a host that could not see its own storage: the first "+ "real answer after this would be terminal on arrival", b.ID, attempts) } } // And when the volume comes back, the very next pass parses the book. if err := os.MkdirAll(filepath.Join(f.root, first.ID), 0o700); err != nil { t.Fatal(err) } f.provision(t, first) if err := f.svc.Parse(f.ctx, first.ID); err != nil { t.Fatal(err) } if got := f.card(t, first.ID); got.Status != "not_started" { t.Fatalf("after the storage came back the book is %q, want not_started", got.Status) } } // Cross-family review of the acceptance dofix (H2): the floor under the library's revision made the // STATUS walk invisible. A book uploaded before somebody else's upload was cancelled carries a // counter below that floor, so `uploading → parsing → not_started` — three increments of its own // number — left `greatest(max, floor)` exactly where it was, and a client obeying the contract drops // every read that is not above what it applied. The book would sit on the screen as "arriving" until // an unrelated event moved the library's number. func TestAStatusChangeIsVisibleEvenUnderTheLibrarysFloor(t *testing.T) { f := newFixture(t) book := f.accept(t, "book.txt", "text") f.provision(t, book) // A cancelled upload raises the floor above the book's own counter. ctx, cancel := context.WithCancel(f.ctx) defer cancel() if _, err := f.svc.Accept(ctx, Intake{UserID: "u1", SourceLang: "zh", TargetLang: "ru", Filename: "abandoned.txt", File: &failingReader{cancel: cancel}}); err == nil { t.Fatal("an upload whose body failed was accepted") } floor := f.libraryRevision(t) var own int64 if err := f.store.Pool().QueryRow(f.ctx, `select revision from books where id = $1`, book.ID).Scan(&own); err != nil { t.Fatal(err) } if own > floor { t.Fatalf("the fixture does not reproduce the case: the book is at %d, above the floor %d", own, floor) } if err := f.svc.Parse(f.ctx, book.ID); err != nil { t.Fatal(err) } if got := f.card(t, book.ID); got.Status != "not_started" { t.Fatalf("the book is %q, want not_started", got.Status) } if got := f.libraryRevision(t); got <= floor { t.Fatalf("the book finished parsing and the library still answers %d (was %d): the client drops "+ "that read and never sees the status", got, floor) } } // Re-check of the dofix (FP5-10): the SHAPE the first guard missed. A volume mounted at BooksDir // leaves an empty directory behind when it is unmounted — the mountpoint — and the boot recreates // that directory anyway, so "does the root exist" answers yes about storage that is gone. Every book // in intake would then be rejected as unreadable and its source deleted. The marker is what tells the // two apart: it is written by an upload and cannot be forged by an unmount or by MkdirAll. func TestAnUnmountedVolumeLooksLikeAnEmptyRootAndStillIsNotTheBooksFault(t *testing.T) { f := newFixture(t) book := f.accept(t, "one.txt", "text") f.provision(t, book) // The upload is what writes the marker, and nothing else does — asserted here rather than assumed, // because a marker that is never written makes every missing directory look like an unmount and // the crash window of the rejection path (FP5-3) would then wait for ever instead of closing. if _, err := os.Stat(filepath.Join(f.root, StorageMarker)); err != nil { t.Fatalf("an accepted upload left no storage marker: %v", err) } // Exactly what an unmount leaves: the root is there, everything under it is not — including the // marker, which lived on the volume. if err := os.RemoveAll(f.root); err != nil { t.Fatal(err) } if err := os.MkdirAll(f.root, 0o750); err != nil { // the boot's own MkdirAll, or the mountpoint t.Fatal(err) } for range parseAttempts * 2 { if err := f.svc.Parse(f.ctx, book.ID); err != nil { t.Fatal(err) } f.now = f.now.Add(claimGrace + time.Minute) } if f.engine.called() != 0 { t.Fatalf("the engine was asked %d times about storage that is not mounted", f.engine.called()) } var status, reason string var attempts int if err := f.store.Pool().QueryRow(f.ctx, `select status, reject_reason, parse_attempts from books where id = $1`, book.ID). Scan(&status, &reason, &attempts); err != nil { t.Fatal(err) } if status != "parsing" || reason != "" { t.Fatalf("an unmounted volume left the book %q/%q: the storage is the host's problem, not the book's", status, reason) } if attempts != 0 { t.Fatalf("the book spent %d attempts while its storage was unmounted", attempts) } // And the marker is not something the platform re-creates behind an operator's back: only an // upload writes it, so the volume coming back is what restores the answer. if _, err := os.Stat(filepath.Join(f.root, StorageMarker)); err == nil { t.Fatal("the marker was re-created while the volume was away: it can no longer tell mounted from not") } } // Re-check of the dofix (tail в): the rest of the same defect. A budget the BOOK gets is not a budget // the PASS still has — the second book of a pass inherited whatever the first one left, so a slow // first parse handed the second a stub of a deadline, the engine call died on it, and the attempt was // spent on a timeout that says nothing about the book. A pass that cannot give a book its whole // budget must not start it: the claim is taken inside Parse, so a book left for the next tick has // spent nothing. func TestAPassTooShortForAParseStartsNoneAtAll(t *testing.T) { f := newFixture(t) first := f.accept(t, "one.txt", "text") second := f.accept(t, "two.txt", "text") f.provision(t, first) f.provision(t, second) f.now = f.now.Add(2 * time.Hour) // past the graces, so the sweep picks both up // What is left of a pass whose first book was slow. ctx, cancel := context.WithTimeout(f.ctx, time.Minute) defer cancel() if err := f.svc.Sweep(ctx); err != nil { t.Fatal(err) } if f.engine.called() != 0 { t.Fatalf("the engine was called %d times on a pass shorter than a parse", f.engine.called()) } for _, b := range []pgstore.Book{first, second} { var attempts int var status string if err := f.store.Pool().QueryRow(f.ctx, `select parse_attempts, status from books where id = $1`, b.ID).Scan(&attempts, &status); err != nil { t.Fatal(err) } if attempts != 0 || status != "parsing" { t.Fatalf("book %s spent %d attempts and is %q after a pass that had no time to parse it", b.ID, attempts, status) } } } // THE PD-367 DOCUMENT: counts that describe a book, and no book. `{chapters_total: 120, // units_total: 400}` with an empty chapter list is exactly what the MATERIALISER has always refused // — and the intake accepted it, founded a book with `chapter_count = 120` over an empty tree, and let // a run be STARTED AND PAID FOR against a count nothing could ever fill. // // The floor is symmetric now, and symmetric is the whole property: one document gets ONE answer from // both ends of the intake. Its class is the seam's — unknown or inconsistent is non-destructive and // loud — so the upload survives and the book is never founded. // // ⚠ This is a COMMISSIONED change to a pinned intake contract (P12 §3.8), not a test bent to a green: // the intake's own battery rode documents with no chapter list, and those fixtures were EXTENDED // (`wholeManifest`) rather than the floor relaxed to admit them. // // Mutation caught: moving the floor below the `ChaptersTotal < 1` branches, or dropping Whole() from // Readable. func TestTheIntakeRefusesTheDocumentItsOwnMaterialiserWouldReject(t *testing.T) { f := newFixture(t) book := f.accept(t, "book.txt", "первая глава") dir := f.provision(t, book) // Counts that describe 120 chapters and 400 pairs; a document carrying neither. f.engine.set(ingest.Manifest{Version: "tm-manifest-v2", ChaptersTotal: 120, UnitsTotal: 400, SourceSHA256: strings.Repeat("ab", 32), ChunkerVersion: "chunk-2026.07"}, nil) if err := f.svc.Parse(f.ctx, book.ID); err != nil { t.Fatal(err) } got := f.card(t, book.ID) if got.Status == "not_started" { t.Fatalf("the intake founded a book on a document its own materialiser refuses: status %q,"+ " chapter_count %d — a run over it can be started and PAID FOR against a count nothing fills", got.Status, got.ChapterCount) } if got.ChapterCount != 0 { t.Errorf("chapter_count was written as %d from a document that carries no chapters", got.ChapterCount) } // NON-DESTRUCTIVE, through the whole budget: a document this build cannot read says nothing about // the user's text. for range parseAttempts + 1 { f.now = f.now.Add(claimGrace + time.Minute) if err := f.svc.Parse(f.ctx, book.ID); err != nil { t.Fatal(err) } } if got := f.card(t, book.ID); got.Status != "rejected" { t.Errorf("the book is %q after its whole budget; the budget still bounds a broken deployment", got.Status) } if _, err := os.Stat(filepath.Join(dir, SourceName+".txt")); err != nil { t.Fatalf("a document this build could not read cost the user their upload: %v", err) } } // PD-213, the LATENT MINE: an engine whose manifest shape moved must never look like a book with // nothing in it. // // The mechanism, and it is one field rename away: `DecodeManifest` does not gate the version, // `json.Unmarshal` ignores unknown fields and leaves missing ones zero — so a v3 manifest that // renamed `chapters_total` decodes into a valid Manifest reading zero chapters and zero units. Zero // and zero is exactly what the intake reads as «the engine read the source and there is no book in // it», which is the ONE verdict that DELETES the user's file after five attempts. The engine would // have parsed the book perfectly. // // The version gate makes that unreachable by turning the same document into `parser_unavailable`, // which keeps the file. It is a SHAPE gate, not a value pin — the counts and identities below it are // stable, and pinning values everywhere would make every engine release a platform release. // // Mutation caught: dropping the version check from Readable, which restores the deletion path. func TestAManifestShapeThisBuildDoesNotKnowNeverDeletesTheUpload(t *testing.T) { f := newFixture(t) book := f.accept(t, "book.txt", "первая глава") dir := f.provision(t, book) // The engine of tomorrow: a version this build has never heard of, and — because the keys moved // under the allowlist — every count decoding to zero. Indistinguishable from an empty book to a // reader that does not ask which shape it is holding. f.engine.set(ingest.Manifest{Version: "tm-manifest-v3"}, nil) for range parseAttempts + 1 { f.now = f.now.Add(claimGrace + time.Minute) if err := f.svc.Parse(f.ctx, book.ID); err != nil { t.Fatal(err) } } if got := f.card(t, book.ID); got.Status != "rejected" { t.Errorf("the book is %q; the budget still bounds a deployment this build cannot read", got.Status) } if _, err := os.Stat(filepath.Join(dir, SourceName+".txt")); err != nil { t.Fatalf("an engine upgrade deleted the user's upload: %v", err) } // And the KNOWN shape, whose counts genuinely agree on nothing, still ends the way it must: that // verdict is a statement about the user's text, and it is only reachable past the gate. empty := f.accept(t, "empty.txt", " ") emptyDir := f.provision(t, empty) f.engine.set(ingest.Manifest{Version: ingest.KnownManifestVersion}, nil) for range parseAttempts + 1 { f.now = f.now.Add(claimGrace + time.Minute) if err := f.svc.Parse(f.ctx, empty.ID); err != nil { t.Fatal(err) } } if _, err := os.Stat(emptyDir); !errors.Is(err, os.ErrNotExist) { t.Errorf("a source with no book in it kept its directory: %v", err) } } // A manifest that counts no chapters and still counts UNITS is not an empty book — it is a document // this build is reading wrong, most likely because a field moved under it. The difference decides // whether the user's upload is deleted, so the two must not share a verdict. // // ⚠ Its FIRST case now dies one line earlier than it used to, and the test is kept rather than // retired because the property it states is unchanged: `tm-manifest-v9` is refused by the version // gate `Readable` added (PD-213) before the count branches are reached at all. The count branch it // was written against is still there and still reachable — by a KNOWN shape whose counts contradict // each other — which is what the second half of this test exercises. The sentence that used to close // this comment ("the engine's manifest is not version-gated on purpose, which leaves exactly this // gap") describes the state before P12: the gate exists now, and it is a SHAPE gate rather than the // value pin that sentence was arguing against. // // Mutation caught: reading `ChaptersTotal < 1` alone as the source being unreadable. func TestAManifestThatContradictsItselfNeverCostsTheUpload(t *testing.T) { f := newFixture(t) book := f.accept(t, "book.txt", "первая глава") dir := f.provision(t, book) // No chapters, yet units and chunks were cut: no book produces this and no engine reports it. f.engine.set(ingest.Manifest{Version: "tm-manifest-v9", UnitsTotal: 4402}, nil) for range parseAttempts + 1 { f.now = f.now.Add(claimGrace + time.Minute) if err := f.svc.Parse(f.ctx, book.ID); err != nil { t.Fatal(err) } } if got := f.card(t, book.ID); got.Status != "rejected" { t.Errorf("the book is %q; the budget still bounds an engine this platform cannot read", got.Status) } if _, err := os.Stat(filepath.Join(dir, SourceName+".txt")); err != nil { t.Fatalf("a manifest this build could not read cost the user their upload: %v", err) } // And the genuinely empty book — every counter agreeing on nothing — still ends the way it must. empty := f.accept(t, "empty.txt", " ") emptyDir := f.provision(t, empty) f.engine.set(ingest.Manifest{Version: "tm-manifest-v2"}, nil) for range parseAttempts + 1 { f.now = f.now.Add(claimGrace + time.Minute) if err := f.svc.Parse(f.ctx, empty.ID); err != nil { t.Fatal(err) } } if got := f.card(t, empty.ID); got.Status != "rejected" { t.Fatalf("a source with no book in it is %q, want rejected", got.Status) } if _, err := os.Stat(emptyDir); !errors.Is(err, os.ErrNotExist) { t.Errorf("the empty book kept its directory: %v", err) } } // The intake CLAIMS the debt it just recorded before it materializes, and stands down when somebody // else holds it. The debt is visible to the materializer's own sweep the moment the parse commits, // and the two would otherwise read one book's source at the same time. // // Mutation caught: refreshing without claiming; refreshing anyway when the claim was refused. func TestTheIntakeClaimsItsOwnDebtBeforeMaterializing(t *testing.T) { f := newFixture(t) reader := &fakeReader{} f.svc.Reader = reader b := f.accept(t, "roman.txt", "第一节\n\n第二节\n") f.provision(t, b) if err := f.svc.Parse(f.ctx, b.ID); err != nil { t.Fatal(err) } if len(reader.claims) != 1 || reader.claims[0].ID != b.ID { t.Fatalf("the intake claimed %+v, want the debt its own parse recorded", reader.claims) } if len(reader.refreshed) != 1 { t.Fatalf("a claimed debt was not materialized: %+v", reader.refreshed) } // …and a debt somebody else is already paying is left alone. second := newFixture(t) held := &fakeReader{unclaimable: true} second.svc.Reader = held b2 := second.accept(t, "roman.txt", "第一节\n\n第二节\n") second.provision(t, b2) if err := second.svc.Parse(second.ctx, b2.ID); err != nil { t.Fatal(err) } if len(held.claims) != 1 || len(held.refreshed) != 0 { t.Errorf("the intake materialized a debt it does not hold: claims=%d refreshes=%d", len(held.claims), len(held.refreshed)) } }