package pgstore import ( "testing" "time" "textmachine/platform/internal/money" ) // announceShape is one progress event of a run, which is where the engine states the shape of the // pipeline it is running: an edit denominator of zero means this deployment has no editor. func announceShape(t *testing.T, s *Store, book string, draftTotal, editTotal int) { t.Helper() tx, err := s.pool.Begin(t.Context()) if err != nil { t.Fatal(err) } defer func() { _ = tx.Rollback(t.Context()) }() if err := recordWaveShape(t.Context(), tx, book, draftTotal, editTotal); err != nil { t.Fatal(err) } if err := tx.Commit(t.Context()); err != nil { t.Fatal(err) } } func shapeEpoch(t *testing.T, s *Store, book string) (epoch int, editor *bool) { t.Helper() if err := s.pool.QueryRow(t.Context(), `select shape_epoch, epoch_editor from books where id = $1`, book).Scan(&epoch, &editor); err != nil { t.Fatal(err) } return epoch, editor } // draftedBook is a book whose chapters are fully DRAFTED and not edited: the state every editor // pipeline passes through, and the state both halves of this file start from. func draftedBook(t *testing.T, s *Store) string { t.Helper() ctx := t.Context() book := readingBook(t, s, ctx, "u1") if err := s.SaveStructure(ctx, book, twoChapters("k1")); err != nil { t.Fatal(err) } exec(t, s, ctx, `update chapters set units_draft_done = units_total where book_id = $1`, book) return book } // THE EDITOR IS REMOVED, and the book stops being frozen half-done (PD-403). // // The old rule read the book's lifetime count through the MONOTONE flag, and the flag cannot come // back down. So a book that had been through an editing pipeline, on a deployment where the operator // has since removed the editor, counted against an edit column that no run would ever fill again: the // count froze, the run closed `ready` at half, and — this is the money — `ChaptersLeft` never fell, // so the purchase scale went on offering chapters that were already translated. The user is sold the // same work twice. // // The owner ruled the cure on 28.08 (D39.165 §2): a change of pipeline shape is an EVENT of the book, // like cutting it again, and the count is legitimately recomputed at the boundary. The monotone flag // is NOT repealed by this — it still refuses to come down, and its pin still stands. What moved is // which fact the count reads. // // Mutation caught: pointing finishedUnits back at the monotone `editWave`; making `epoch_editor` // accumulate (`or`) instead of assign. func TestRemovingTheEditorRecomputesTheBooksCountInsteadOfFreezingIt(t *testing.T) { s, ctx := testDB(t) book := draftedBook(t, s) // Epoch 1: the deployment HAS an editor. Nothing is edited yet, so nothing is finished. announceShape(t, s, book, 2, 2) before, _, err := s.GetBook(ctx, "u1", book) if err != nil { t.Fatal(err) } if before.ChaptersDone != 0 { t.Fatalf("under an editor a merely drafted book reads %d done, want 0", before.ChaptersDone) } epoch, editor := shapeEpoch(t, s, book) if epoch != 0 || editor == nil || !*editor { t.Fatalf("first announcement left epoch=%d editor=%v, want the epoch unmoved and the shape recorded", epoch, editor) } // The operator removes the editor and the next run announces a zero edit denominator. announceShape(t, s, book, 2, 0) after, _, err := s.GetBook(ctx, "u1", book) if err != nil { t.Fatal(err) } if after.ChaptersDone != 2 { t.Errorf("with the editor gone the book still reads %d/%d done, want 2: it is frozen against an"+ " edit column no run will fill, and the purchase scale goes on selling chapters already translated", after.ChaptersDone, after.ChapterCount) } // The recomputation ANNOUNCES itself, or the client cannot tell it from a count walking backwards. if after.ShapeEpoch == before.ShapeEpoch { t.Errorf("the count was recomputed at epoch %d without moving it: the jump is indistinguishable"+ " from an illegal move and the canon's amnesty is unobservable", after.ShapeEpoch) } // ⚠ The monotone flag is NOT repealed. D39.153 §4б still holds and its pin still stands. var flag *bool if err := s.pool.QueryRow(ctx, `select edit_wave from books where id = $1`, book).Scan(&flag); err != nil { t.Fatal(err) } if flag == nil || !*flag { t.Errorf("edit_wave came down to %v: the historical flag is monotone (D39.153 §4б) and this"+ " work does not repeal it — only the LIFETIME COUNT stopped being read off it", flag) } // And the OFFER, which is the money half of PD-403: what is left to buy must have fallen. // // ⚠ Read off `ReadBookForOrder` since the order form: the single `ChaptersLeft` count that used // to answer this is gone with the ceiling scale, and what replaced it is the list of chapters // still to deliver. The question is the same one — a fully translated book must offer nothing — // and it is the one PD-403 was about: the service went on offering, and taking a hold for, // chapters it had already translated. po, err := s.ReadBookForOrder(ctx, "u1", book) if err != nil { t.Fatal(err) } if len(po.Remaining) != 0 { t.Errorf("the order form still offers %d chapters of a fully translated book: %+v", len(po.Remaining), po.Remaining) } } // THE EDITOR IS ADDED — the one direction the old flag could move — and the count still recomputes, // but it now says so (PD-404). // // The flag flips false→true on the first progress event of the run that has an editor, and the // lifetime count moves to the edit column in the same transaction: a book showing 2/2 shows 0/2, // backwards, inside ONE `structure_version` — which the canon forbade in as many words. The owner's // ruling makes the jump legitimate; what makes it legible is that the epoch moves with it, so a // client sees a new generation rather than a server walking a counter down. // // Mutation caught: not counting a false→true boundary (an epoch that only moves one way). func TestAddingTheEditorMovesTheEpochWithTheCountItRecomputes(t *testing.T) { s, ctx := testDB(t) book := draftedBook(t, s) // Epoch 0: a draft-only deployment. Every drafted chapter is finished, because drafting is the // last pass the book gets. announceShape(t, s, book, 2, 0) before, _, err := s.GetBook(ctx, "u1", book) if err != nil { t.Fatal(err) } if before.ChaptersDone != 2 { t.Fatalf("a draft-only deployment reads %d done on a fully drafted book, want 2", before.ChaptersDone) } // The operator ADDS an editor. The work already done is now half-work, and the count says so. announceShape(t, s, book, 2, 2) after, _, err := s.GetBook(ctx, "u1", book) if err != nil { t.Fatal(err) } if after.ChaptersDone != 0 { t.Errorf("after the editor arrived the book reads %d done, want 0: a drafted chapter is no"+ " longer finished work", after.ChaptersDone) } if after.ShapeEpoch <= before.ShapeEpoch { t.Errorf("the count went %d → %d — BACKWARDS inside one structure_version — and the epoch"+ " stayed at %d: without the boundary this is exactly what the canon forbids", before.ChaptersDone, after.ChaptersDone, after.ShapeEpoch) } } // The epoch counts BOUNDARIES, not announcements: a run re-stating the shape it already has moves // nothing. Otherwise every progress event of every run would look to a client like a recomputation, // and the signal would mean nothing by the time it mattered. // // Mutation caught: incrementing unconditionally, or on the FIRST announcement (which crosses nothing). func TestTheEpochMovesOnBoundariesAndNotOnEveryAnnouncement(t *testing.T) { s, _ := testDB(t) book := draftedBook(t, s) announceShape(t, s, book, 2, 2) first, _ := shapeEpoch(t, s, book) for range 3 { announceShape(t, s, book, 2, 2) } same, _ := shapeEpoch(t, s, book) if same != first { t.Errorf("three restatements of the same shape moved the epoch %d → %d", first, same) } announceShape(t, s, book, 2, 0) crossed, _ := shapeEpoch(t, s, book) if crossed != first+1 { t.Errorf("one boundary moved the epoch %d → %d, want exactly one step", first, crossed) } // …and back again is another boundary, not a return to the old number: the epoch is a generation // counter, not a state. announceShape(t, s, book, 2, 2) back, _ := shapeEpoch(t, s, book) if back != crossed+1 { t.Errorf("crossing back left the epoch at %d, want %d: it counts crossings, not shapes", back, crossed+1) } } // THE RUN'S BAR is NOT on the epoch, and this test is what holds that line. // // ⚠ Its predecessor asserted the opposite — that a run on a deployment which dropped the editor can // reach one — and that was this pack's own mistake, caught by its adversarial pass and measured on a // live database: pointing the bar at the ASSIGNABLE epoch let one run read 4/4 and then 2/2 across // its own two attempts, the same row, the same `structure_version`, the fraction walking backwards. // The canon holds a run's bar to one monotonic fraction over the run's whole work (row 200), so the // bar stays on the MONOTONE `books.edit_wave` and only the BOOK's lifetime count moved to the epoch. // // What that leaves open is stated in the register rather than hidden here: on a deployment where the // editor was removed, a run's bar still tariffs an edit wave that will not happen (PD-403's second // half). Closing it needs a per-RUN record of the shape that run works under — and the shape is not // known at StartRun, because the engine announces it with the run's FIRST progress event, which is // precisely what PD-401 found. That is a decision of its own size. // // Mutation caught: pointing runDone/runTotal/runStage at epochWave. func TestTheRunsBarIsMonotoneAcrossAShapeBoundaryItSpans(t *testing.T) { s, ctx := testDB(t) book := draftedBook(t, s) // An editor deployment: the run buys two chapters and the bar tariffs both waves. announceShape(t, s, book, 2, 2) at := time.Now().UTC().Truncate(time.Microsecond) run, err := s.StartRun(ctx, StartRunInput{UserID: "u1", BookID: book, OrderedChapters: 2, Ceiling: money.MicroUSD(300_000), Now: at}, 0, nil) if err != nil { t.Fatal(err) } exec(t, s, ctx, `update chapters set units_edit_done = units_total where book_id = $1`, book) before, err := s.ReadRun(ctx, "u1", run.ID) if err != nil { t.Fatal(err) } // The operator removes the editor and the run's SECOND attempt announces a draft-only shape. announceShape(t, s, book, 2, 0) after, err := s.ReadRun(ctx, "u1", run.ID) if err != nil { t.Fatal(err) } if after.Progress.Done < before.Progress.Done || after.Progress.Total < before.Progress.Total { t.Errorf("the bar of ONE run walked backwards across a shape boundary it spans: %d/%d → %d/%d."+ " The canon holds a run's bar to one monotonic fraction, and a run does not get to un-do"+ " work it was paid for because the deployment changed shape under it", before.Progress.Done, before.Progress.Total, after.Progress.Done, after.Progress.Total) } // …while the BOOK's count DID move: that is the ruling, and the epoch announces it. card, _, err := s.GetBook(ctx, "u1", book) if err != nil { t.Fatal(err) } if card.ShapeEpoch == 0 { t.Error("the book crossed a shape boundary and its epoch did not move") } }