textmachine/backend/internal/pipeline/priceprojection_test.go

700 lines
33 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 pipeline
import (
"context"
"encoding/json"
"os"
"strings"
"testing"
"textmachine/backend/internal/config"
"textmachine/backend/internal/ledger"
"textmachine/backend/internal/chunk"
"textmachine/backend/internal/obs"
)
// priceprojection_test.go: the acceptance battery for backlog row 278 — the engine says what a book will
// cost and what ceiling it needs BEFORE anything is bought, and it says whether it believes its own
// chapter cut. $0 throughout: `manifest` makes no provider call at all.
// TestTheManifestPricesABookNobodyHasBought is the whole point of the projection: the question is asked
// in the state where the engine's OTHER price figure (projectBookUSD, extrapolated from units already
// paid for) can only answer zero.
func TestTheManifestPricesABookNobodyHasBought(t *testing.T) {
rec := &reqRec{}
srv := newJSONProvider(rec, draftEdit)
defer srv.Close()
bookPath := setupProjectOpts(t, srv.URL, projectOpts{source: cjkSource(3, 1400)})
r := newRunner(t, bookPath)
defer r.Close()
m, err := r.BuildAndPersistManifest()
if err != nil {
t.Fatal(err)
}
if rec.count() != 0 {
t.Fatalf("the projection is a $0 verb and reached a provider %d time(s)", rec.count())
}
if m.Price == nil {
t.Fatal("a book with prices and stages must be priced")
}
if m.Price.ExpectedUSD <= 0 {
t.Fatalf("the whole book prices at %v", m.Price.ExpectedUSD)
}
// ⛔ THE FIGURE THE WALL IS MADE OF. A ceiling under this admits nothing at all, whatever else is
// true, and publishing it is what lets a platform stop offering orders that cannot move.
if m.Price.StepMaxUSD <= 0 {
t.Fatal("step_max_usd is the largest indivisible reservation and cannot be zero for a priced book")
}
perUnit := m.Chapters[0].Units[0].Price
if perUnit == nil {
t.Fatal("every unit must carry its own size and price")
}
// ⛔ THE PROJECTION IS CHECKED AGAINST THE EXECUTOR, not against itself. step_max_usd is the LARGEST
// reservation any call of this book can ask for, so it must be at least the reservation the executor
// computes for the book's very first call — a number derived here from the executor's own arithmetic
// (baseMaxTokensFor + ledger.EstimateUSD), which is the same pair the projection uses.
//
// ⚠ It is deliberately NOT compared with a unit's expected BILL, which was this test's first
// assertion and was simply wrong: a unit's bill sums EVERY stage of the pipeline while step_max is ONE
// call, so a two-stage unit legitimately costs more than the largest single step. The comparison that
// means something is against a call.
firstCall := callCeilingUSD(t, r, 1.0)
if m.Price.StepMaxUSD < firstCall {
t.Errorf("step_max_usd (%v) is under the reservation the executor makes for the first call (%v) — a ceiling set to it would admit nothing",
m.Price.StepMaxUSD, firstCall)
}
// Sizes: RUNES including spaces, split by the engine's one token-sizing taxonomy. A space is counted
// in SourceChars and in NEITHER class, which is the taxonomy — so the two classes bound the total from
// below rather than summing to it.
if perUnit.SourceChars <= 0 {
t.Errorf("a unit reports no source characters: %+v", perUnit)
}
if perUnit.SourceCharsDense+perUnit.SourceCharsSparse > perUnit.SourceChars {
t.Errorf("the dense/sparse split exceeds the character count it splits: %+v", perUnit)
}
if perUnit.SourceCharsDense == 0 {
t.Errorf("a Han unit reports no dense characters: %+v", perUnit)
}
// The chapter roll-up must be the sum of its units, and the book the sum of its chapters plus the
// once-per-book charge. A roll-up that drifts from its parts is worse than no roll-up.
var unitSum float64
var charSum int
for _, u := range m.Chapters[0].Units {
unitSum += u.Price.ExpectedUSD
charSum += u.Price.SourceChars
}
if diff := m.Chapters[0].Price.ExpectedUSD - unitSum; diff > 1e-12 || diff < -1e-12 {
t.Errorf("the chapter roll-up (%v) is not the sum of its units (%v)", m.Chapters[0].Price.ExpectedUSD, unitSum)
}
if m.Chapters[0].Price.SourceChars != charSum {
t.Errorf("the chapter's character count (%d) is not the sum of its units' (%d)", m.Chapters[0].Price.SourceChars, charSum)
}
var bookSum float64
for _, c := range m.Chapters {
bookSum += c.Price.ExpectedUSD
}
if diff := m.Price.ExpectedUSD - (bookSum + m.Price.BookOnceUSD); diff > 1e-12 || diff < -1e-12 {
t.Errorf("the book price (%v) is not its chapters (%v) plus the once-per-book charge (%v)",
m.Price.ExpectedUSD, bookSum, m.Price.BookOnceUSD)
}
}
// TestTheProjectionIsNotAPerChapterConstant is the row's own prohibition, asserted rather than promised:
// «считается из СВОИХ цен и фертильности, БЕЗ константы». A longer chapter must cost more, in proportion
// to what is actually in it.
func TestTheProjectionIsNotAPerChapterConstant(t *testing.T) {
rec := &reqRec{}
srv := newJSONProvider(rec, draftEdit)
defer srv.Close()
// Two chapters of very different length in the same book.
src := strings.Repeat("蛊", 400) + "\f" + strings.Repeat("蛊", 3200)
bookPath := setupProjectOpts(t, srv.URL, projectOpts{source: src})
r := newRunner(t, bookPath)
defer r.Close()
m, err := r.BuildAndPersistManifest()
if err != nil {
t.Fatal(err)
}
if len(m.Chapters) != 2 {
t.Fatalf("fixture drifted: want 2 chapters, got %d", len(m.Chapters))
}
short, long := m.Chapters[0].Price, m.Chapters[1].Price
if short == nil || long == nil {
t.Fatal("both chapters must be priced")
}
if long.ExpectedUSD <= short.ExpectedUSD {
t.Fatalf("an eight-times-longer chapter is not dearer: short=%v long=%v — the projection is a constant", short.ExpectedUSD, long.ExpectedUSD)
}
if long.SourceChars <= short.SourceChars {
t.Fatalf("the character counts do not track the text: short=%d long=%d", short.SourceChars, long.SourceChars)
}
}
// TestTheCutSaysWhetherItWasReadOrGuessed pins `structure` on the three values a TXT source can produce,
// because the field exists to stop an order phrased in chapters being offered against a guess.
//
// The fourth value, `declared`, is NOT reachable from a txt and is not pinned here: it needs a format that
// states its own chapter structure, so it lives with the EPUB tables of contents
// (TestEPUBNavDrawsChaptersNotTheSpine, TestEPUB2NCXDrawsChaptersWhenThereIsNoNav).
func TestTheCutSaysWhetherItWasReadOrGuessed(t *testing.T) {
for _, tc := range []struct {
name string
source string
want string
}{
// A form feed is the ASCII PAGE break — a separator the file carries, not a statement that a chapter
// begins here. The engine cut on it because nothing better was found, and says so.
{"a form feed is a separator, not a chapter", "ГЛАВАОДИН\fГЛАВАДВА", chunk.StructureDelimited},
// CJK chapter headers are matched in the prose: the engine INFERRED these boundaries.
{"a matched header line is a guess", "第一章\n" + strings.Repeat("蛊", 200) + "\n第二章\n" + strings.Repeat("蛊", 200), chunk.StructureDetected},
// One span of text: any finer unit of ordering would be invented here.
{"one chapter is no structure at all", strings.Repeat("蛊", 300), chunk.StructureNone},
} {
t.Run(tc.name, func(t *testing.T) {
rec := &reqRec{}
srv := newJSONProvider(rec, draftEdit)
defer srv.Close()
bookPath := setupProjectOpts(t, srv.URL, projectOpts{source: tc.source})
r := newRunner(t, bookPath)
defer r.Close()
m, err := r.BuildAndPersistManifest()
if err != nil {
t.Fatal(err)
}
if m.Structure != tc.want {
t.Errorf("structure = %q, want %q (chapters=%d)", m.Structure, tc.want, m.ChaptersTotal)
}
})
}
}
// TestTheProjectionReachesBothSurfacesAndTheyAgree is the reader/writer axis of this pack, nailed from
// both sides by one act.
//
// ⚠ THE CLASS IT CLOSES WAS FOUND THREE TIMES IN THE PACK BEFORE THIS ONE: a fix applied to the surface
// that READS a value while the surface that WRITES it kept its own copy, with every test still green.
// Here the manifest writes the price and `status` reads it, so the assertion is that they are the same
// bytes rather than two derivations that happen to agree today.
func TestTheProjectionReachesBothSurfacesAndTheyAgree(t *testing.T) {
rec := &reqRec{}
srv := newJSONProvider(rec, draftEdit)
defer srv.Close()
bookPath := setupProjectOpts(t, srv.URL, projectOpts{source: cjkSource(2, 1400)})
r := newRunner(t, bookPath)
defer r.Close()
m, err := r.BuildAndPersistManifest()
if err != nil {
t.Fatal(err)
}
rep, err := r.Status(context.Background())
if err != nil {
t.Fatal(err)
}
if rep.Price == nil {
t.Fatal("`status` publishes no price: the surface a buyer's platform reads before deciding cannot answer the question")
}
if rep.Price.ExpectedUSD != m.Price.ExpectedUSD || rep.Price.StepMaxUSD != m.Price.StepMaxUSD || rep.Price.BookOnceUSD != m.Price.BookOnceUSD {
t.Errorf("status and manifest quote different prices for one book:\n status=%+v\n manifest=%+v", *rep.Price, *m.Price)
}
if rep.Structure != m.Structure {
t.Errorf("status says the cut is %q and the manifest says %q", rep.Structure, m.Structure)
}
// And the price must not be confused with the a-posteriori figure beside it: an unrun book has spent
// nothing, so that one is zero while this one is not.
if rep.ProjectedBookUSD != 0 {
t.Errorf("a book that has never run has no measured projection, got %v", rep.ProjectedBookUSD)
}
if rep.Price.ExpectedUSD == 0 {
t.Error("…and the a-priori projection must be the one that answers")
}
}
// TestTheManifestDocumentVersionDoesNotMoveForAnAdditiveField pins the seam rule the pack states and the
// document's own comment argues: a version bump makes every stored sidecar discard itself and re-chunk
// every book once, and it buys nothing for a field that cannot be half-read.
func TestTheManifestDocumentVersionDoesNotMoveForAnAdditiveField(t *testing.T) {
if manifestVersion != "tm-manifest-v2" {
t.Fatalf("the manifest document version moved to %q — adding derived fields is the ratified «engine-first» compatibility path and is not a shape break", manifestVersion)
}
}
// TestTheStoredManifestCarriesThePriceOnDisk reads the artifact as a CONSUMER does — bytes off the
// filesystem — rather than asking the engine's own struct what it holds.
func TestTheStoredManifestCarriesThePriceOnDisk(t *testing.T) {
rec := &reqRec{}
srv := newJSONProvider(rec, draftEdit)
defer srv.Close()
bookPath := setupProjectOpts(t, srv.URL, projectOpts{source: cjkSource(2, 1400)})
r := newRunner(t, bookPath)
if _, err := r.BuildAndPersistManifest(); err != nil {
t.Fatal(err)
}
path := r.ManifestPath()
r.Close()
raw, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
var doc struct {
Structure string `json:"structure"`
Price *struct {
ExpectedUSD float64 `json:"expected_usd"`
BookOnceUSD float64 `json:"book_once_usd"`
StepMaxUSD float64 `json:"step_max_usd"`
SourceChars int `json:"source_chars"`
} `json:"price"`
Chapters []struct {
Units []struct {
Price *struct {
SourceChars int `json:"source_chars"`
SourceCharsDense int `json:"source_chars_dense"`
SourceCharsSparse int `json:"source_chars_sparse"`
ExpectedUSD float64 `json:"expected_usd"`
} `json:"price"`
} `json:"units"`
} `json:"chapters"`
}
if err := json.Unmarshal(raw, &doc); err != nil {
t.Fatalf("the persisted manifest is not readable: %v", err)
}
if doc.Structure == "" {
t.Error("the persisted document does not say where its chapter boundaries came from")
}
if doc.Price == nil || doc.Price.StepMaxUSD <= 0 || doc.Price.SourceChars <= 0 {
t.Fatalf("the persisted document carries no usable price: %+v", doc.Price)
}
if len(doc.Chapters) == 0 || len(doc.Chapters[0].Units) == 0 || doc.Chapters[0].Units[0].Price == nil {
t.Fatal("the persisted units carry no price")
}
u := doc.Chapters[0].Units[0].Price
if u.SourceChars <= 0 || u.SourceCharsDense <= 0 || u.ExpectedUSD <= 0 {
t.Errorf("a persisted unit's price is not usable: %+v", u)
}
}
// TestTheProjectionUsesTheExecutorsOwnSizing is the anti-drift pin. The projection and the executor share
// one definition of a stage's output budget (baseMaxTokensFor), and the point of sharing it is that a
// change to sizing cannot move one without the other.
func TestTheProjectionUsesTheExecutorsOwnSizing(t *testing.T) {
rec := &reqRec{}
srv := newJSONProvider(rec, draftEdit)
defer srv.Close()
bookPath := setupProjectOpts(t, srv.URL, projectOpts{source: cjkSource(1, 1400), minMaxTokens: 4096})
r := newRunner(t, bookPath)
defer r.Close()
m, err := r.BuildAndPersistManifest()
if err != nil {
t.Fatal(err)
}
withFloor := m.Price.StepMaxUSD
r.Close()
// The SAME book with a much smaller floor must project a smaller largest step: the floor is part of
// the executor's sizing, so a projection that ignored it would return the same number twice.
bookPath2 := setupProjectOpts(t, srv.URL, projectOpts{source: cjkSource(1, 1400), minMaxTokens: 256})
r2 := newRunner(t, bookPath2)
defer r2.Close()
m2, err := r2.BuildAndPersistManifest()
if err != nil {
t.Fatal(err)
}
if m2.Price.StepMaxUSD >= withFloor {
t.Errorf("step_max_usd ignores the executor's own output-budget floor: floor 4096 → %v, floor 256 → %v",
withFloor, m2.Price.StepMaxUSD)
}
}
// TestARunRewritesTheProjectionToo: the price is not a `manifest`-command curiosity — a translate rebuilds
// the sidecar, and a run that dropped the price would leave `status` blind for exactly the books that
// have been paid for.
func TestARunRewritesTheProjectionToo(t *testing.T) {
rec := &reqRec{}
srv := newJSONProvider(rec, draftEdit)
defer srv.Close()
bookPath := setupProjectOpts(t, srv.URL, projectOpts{source: cjkSource(2, 1400)})
ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()})
r := newRunner(t, bookPath)
defer r.Close()
if _, err := r.TranslateBook(ctx); err != nil {
t.Fatal(err)
}
m := r.loadManifest()
if m == nil {
t.Fatal("the run wrote no manifest")
}
if m.Price == nil || m.Price.StepMaxUSD <= 0 {
t.Fatalf("a run's own manifest carries no price: %+v", m.Price)
}
if m.Structure == "" {
t.Error("a run's own manifest does not say where its chapter boundaries came from")
}
}
// TestStepMaxCountsTheRetryThatDoesNotDegrade is the half of step_max_usd that a ceiling sized for
// attempt 0 gets wrong, and it is the expensive half.
//
// ⛔ maxTokensForAttempt DOUBLES the output budget on every regeneration, and a refused RETRY does not
// degrade — it halts the book. So a ceiling that admits a unit's first call and not its remedy leaves
// that unit unable to finish EVER: each resume replays the flagged attempt 0 for free and dies on
// attempt 1 again, while the run departs `paused` asking for money that would not help. Publishing a
// step_max that ignored retries would be publishing exactly that ceiling.
func TestStepMaxCountsTheRetryThatDoesNotDegrade(t *testing.T) {
rec := &reqRec{}
srv := newJSONProvider(rec, draftEdit)
defer srv.Close()
// The same book twice, differing ONLY in how many regenerations a stage is allowed.
noRetry := setupProjectOpts(t, srv.URL, projectOpts{source: cjkSource(1, 1400), regenerate: 0})
withRetry := setupProjectOpts(t, srv.URL, projectOpts{source: cjkSource(1, 1400), regenerate: 1})
r1 := newRunner(t, noRetry)
m1, err := r1.BuildAndPersistManifest()
if err != nil {
t.Fatal(err)
}
r1.Close()
r2 := newRunner(t, withRetry)
defer r2.Close()
m2, err := r2.BuildAndPersistManifest()
if err != nil {
t.Fatal(err)
}
if m2.Price.StepMaxUSD <= m1.Price.StepMaxUSD {
t.Fatalf("step_max_usd ignores the regeneration budget: regenerate=0 → %v, regenerate=1 → %v. A ceiling set to the smaller number admits a unit's first call and refuses its remedy on every resume, forever",
m1.Price.StepMaxUSD, m2.Price.StepMaxUSD)
}
// And the EXPECTED bill must NOT move: a regeneration is a remedy, not a forecast, and folding it
// into the headline price would quote a bad run as the rule.
if m2.Price.ExpectedUSD != m1.Price.ExpectedUSD {
t.Errorf("the expected BILL moved with the retry budget (%v → %v): a conditional remedy is not part of what a book is expected to cost",
m1.Price.ExpectedUSD, m2.Price.ExpectedUSD)
}
}
// planFixture builds a Runner with hand-made templates and prices — the smallest thing that can answer
// «what does the projection put on the wire for ONE call», without a project directory or a server.
func planFixture(t *testing.T, tpl *PromptTemplate, fewShot *bool) *pricePlan {
t.Helper()
pricer, err := ledger.NewPricer(nil, ledger.ModelPrice{InputPerM: 1, OutputPerM: 1})
if err != nil {
t.Fatal(err)
}
// ⚠ THROUGH THE SEAM, NOT BY HAND — the project's own vet analyzer refuses a hand-built config.Stage
// outside the config package (archguard.StageSeam, D39.87: a literal silently drops reasoning and
// temperature), and it caught this fixture's first version. There IS an exemption list and this file
// is not on it, deliberately: the rule is right, the seam costs nothing here, and an exemption added
// to make one's own test compile is a gate weakened for convenience. Only `few_shot` is set
// afterwards, because that is the very field under test and the seam leaves it zero on purpose.
st := config.InternalCall{Name: "s", Role: "editor"}.Stage()
st.FewShot = fewShot
r := &Runner{
Models: &config.Models{},
Pricer: pricer,
Pipeline: &config.Pipeline{Stages: []config.Stage{st}},
templates: map[string]*PromptTemplate{"s": tpl},
}
plan := r.pricePlanFor()
if plan == nil || len(plan.stages) != 1 {
t.Fatal("the fixture resolved no priceable stage")
}
return plan
}
// TestTheProjectionChargesTheFewShotBlockOnlyWhenTheStageSendsIt pins that the projection asks the
// EXECUTOR'S question about the few-shot block rather than adding the template's fields up.
//
// ⛔ WHAT IT PREVENTS IS A SILENT OVER-CHARGE ON EVERY CALL OF EVERY BOOK ON AN ARM. SystemFor appends the
// block only when the stage's `few_shot` toggle is on, and the shipped c1 editor has it OFF while its
// template still CARRIES the block — over a thousand bytes. A projection that summed System+FewShot+User
// would bill that block on every edit call of the projection, and it would do it invisibly, because a
// price that is 20 % too high looks exactly like a price.
func TestTheProjectionChargesTheFewShotBlockOnlyWhenTheStageSendsIt(t *testing.T) {
tpl := &PromptTemplate{System: "SYSTEM", FewShot: strings.Repeat("Ф", 3000), User: "USER"}
off := false
on := true
withBlock := planFixture(t, tpl, &on).stages[0].tplTokens
withoutBlock := planFixture(t, tpl, &off).stages[0].tplTokens
if withBlock <= withoutBlock {
t.Fatalf("the few-shot block is not priced when the stage sends it: on=%d off=%d", withBlock, withoutBlock)
}
// And the toggle's default is ON (absent means on — every existing config keeps its examples), so a
// nil toggle must price like an explicit true.
if def := planFixture(t, tpl, nil).stages[0].tplTokens; def != withBlock {
t.Errorf("an absent few_shot toggle must price as ON (the executor's default), got %d want %d", def, withBlock)
}
}
// TestTheProjectionChargesTheSourceToEveryStageThatAsksForIt pins the half that was MISSING and that bent
// the number in the dangerous direction.
//
// ⛔ The executor renders EVERY stage with `RenderVars{Text: ch.Text, Draft: prev}`, so a template holding
// `{{text}}` receives the source whatever its position — and the shipped editor is BILINGUAL by ratified
// design (D30.1), which is the whole reason it can bind a canon to its source term. A projection that
// gave the later stages only the draft dropped a source-sized block from the prompt of every edit call:
// roughly a sixth of a unit's expected bill, UNDER-stated — the direction that lets a platform sell a
// book for less than it costs.
func TestTheProjectionChargesTheSourceToEveryStageThatAsksForIt(t *testing.T) {
bilingual := planFixture(t, &PromptTemplate{System: "S", User: "translate " + sourcePlaceholder}, nil).stages[0]
mono := planFixture(t, &PromptTemplate{System: "S", User: "edit {{draft}}"}, nil).stages[0]
if !bilingual.carriesSource {
t.Fatal("a template holding {{text}} puts the source on the wire and the projection must know it")
}
if mono.carriesSource {
t.Fatal("a template with no {{text}} never receives the source; charging it would over-state the arm")
}
p := &pricePlan{inject: 100}
const source, draft = 500, 300
// A LATER stage reading a draft: the source rides too, and is charged.
if got, want := p.promptTokens(bilingual, source, draft, false), bilingual.tplTokens+draft+100+source; got != want {
t.Errorf("a bilingual later stage prices %d, want %d — the source is missing from the prompt", got, want)
}
// The MONOLINGUAL arm on the same shape is priced without it.
if got, want := p.promptTokens(mono, source, draft, false), mono.tplTokens+draft+100; got != want {
t.Errorf("a monolingual arm prices %d, want %d — it was charged for a source it never receives", got, want)
}
// ⚠ And the FIRST stage is not charged twice: its input IS the source.
if got, want := p.promptTokens(bilingual, source, source, true), bilingual.tplTokens+source+100; got != want {
t.Errorf("the first stage prices %d, want %d — the source was counted twice", got, want)
}
// ⛔ IDENTITY IS STATED, NOT COUNTED. A later stage whose draft happens to have exactly the source's
// token count — a pair whose fertility sits near 1.0 makes one — is still a LATER stage, and the
// source it receives must still be charged. Comparing the two numbers, as the first version did, made
// this case silently free (acceptance V2-8).
if got, want := p.promptTokens(bilingual, source, source, false), bilingual.tplTokens+source+100+source; got != want {
t.Errorf("a later stage whose draft coincides in size with the source prices %d, want %d — identity was inferred from equal numbers instead of being stated", got, want)
}
}
// TestTheDraftIsPricedPerMemberChunkAndTheEditPerUnit pins the GRANULARITY of the projection against the
// granularity of the executor.
//
// ⛔ THE WAVE DRIVER FANS THE DRAFT OVER CHUNKS AND THE EDIT OVER UNITS (runDraftChunk / runEditUnit), and
// collapsing that biases the number in both directions at once. The template and the injection ride EVERY
// member's draft call, so charging them once per unit UNDER-states the bill; and a draft call's output
// budget is sized from ONE chunk, so sizing it from the whole unit OVER-states the largest single
// reservation — the number a platform sets its minimum purchase by. On the shipped arm the second is
// hidden by the model floor and the first is a couple of percent; on an arm without a floor neither is.
func TestTheDraftIsPricedPerMemberChunkAndTheEditPerUnit(t *testing.T) {
rec := &reqRec{}
srv := newJSONProvider(rec, draftEdit)
defer srv.Close()
bookPath := setupProjectOpts(t, srv.URL, projectOpts{source: cjkSource(1, 1400)})
r := newRunner(t, bookPath)
defer r.Close()
plan := r.pricePlanFor()
if plan == nil {
t.Fatal("the fixture resolves no priceable stage")
}
half := strings.Repeat("蛊", 700)
one := editUnit{Members: []chunk.Chunk{{Text: half + half}}}
two := editUnit{Members: []chunk.Chunk{{Text: half}, {Text: half}}}
pOne, pTwo := plan.projectUnit(one), plan.projectUnit(two)
// Same text up to the JOIN the edit wave puts between members — so the difference in the price below
// is about how many CALLS the draft takes, not about how much text there is.
if diff := pTwo.SourceChars - pOne.SourceChars; diff != len([]rune(unitJoinSeparator)) {
t.Fatalf("fixture drifted: the two units must hold the same text apart from one member join (%d vs %d chars, diff %d)",
pOne.SourceChars, pTwo.SourceChars, diff)
}
if pTwo.ExpectedUSD <= pOne.ExpectedUSD {
t.Errorf("a two-member unit costs %v and a one-member unit of the same text costs %v — the per-call template and injection are being charged once per UNIT instead of once per member draft call",
pTwo.ExpectedUSD, pOne.ExpectedUSD)
}
// …and the largest single RESERVATION goes the other way: a draft call is sized from one member, so
// splitting the text into two chunks makes the biggest draft step SMALLER, never larger.
stepOne := r.stepMaxForUnit(plan, one, pOne)
stepTwo := r.stepMaxForUnit(plan, two, pTwo)
if stepTwo > stepOne {
t.Errorf("the two-member unit reports a LARGER indivisible step (%v) than the one-member unit holding the same text (%v) — the draft step is being sized from the whole unit rather than from one call",
stepTwo, stepOne)
}
}
// TestTheBookLevelChargeAppearsOnlyWhenTheBookWillPayIt pins bookOnceUSD, which until now only ever
// asserted its own zero: terminology is off in every project fixture, so `return 0` unconditionally
// survived every test in the tree.
//
// ⛔ THE FIELD EXISTS SO A SHORT BOOK IS NOT UNDER-PRICED (row 278): the consolidation reads the whole
// book's drafts ONCE, so charging it per unit makes a three-chapter book look cheap exactly where the
// error hurts. And it is CONDITIONAL on the pair «the gate is on AND the contrast artifact is named» —
// the operator-facing trap the deployment runbook documents: lose the FILE and the run refuses loudly,
// lose the KEY in the config and the published price quietly drops.
func TestTheBookLevelChargeAppearsOnlyWhenTheBookWillPayIt(t *testing.T) {
priced := func(gate config.TerminologyGate, contrast string) *Runner {
pricer, err := ledger.NewPricer(nil, ledger.ModelPrice{InputPerM: 1, OutputPerM: 1})
if err != nil {
t.Fatal(err)
}
st := config.InternalCall{Name: "s", Role: roleTranslator}.Stage()
return &Runner{
Models: &config.Models{},
Pricer: pricer,
Pipeline: &config.Pipeline{
Stages: []config.Stage{st},
Gates: config.Gates{Terminology: gate},
Mining: config.Mining{ContrastPath: contrast},
},
templates: map[string]*PromptTemplate{"s": {System: "S", User: "U " + sourcePlaceholder}},
}
}
// ⚠ `ClassifyTypes` IS SET HERE AND WAS NOT BEFORE, and that omission is why this test used to pin the
// WRONG number: with the phase off the classifier cannot run, so its budget is not part of what the
// book will pay — and the assertion below demanded that it be added anyway. A test defending the
// defect it is meant to guard survives every review that reads green (acceptance V2-5).
on := config.TerminologyGate{Enabled: true, ClassifyTypes: true, BudgetUSD: 0.25, ClassifyBudgetUSD: 0.75}
// BOTH halves of the condition, each on its own: the gate alone is not enough, and the artifact alone
// is not either — the pass does not run without both, so the book does not pay for it.
if usd := priced(on, "").bookOnceUSD(); usd != 0 {
t.Errorf("the gate is on but no contrast artifact is named: the pass cannot run and the book must not be charged, got %v", usd)
}
if usd := priced(config.TerminologyGate{}, "/corpus").bookOnceUSD(); usd != 0 {
t.Errorf("the artifact is named but the gate is off: got %v", usd)
}
// AND THE PHASE-OFF CASE, which is the one that was wrong: the classifier cannot run, so its budget is
// not part of what the book will pay.
phaseOff := on
phaseOff.ClassifyTypes = false
if usd := priced(phaseOff, "/corpus").bookOnceUSD(); usd != 0.25 {
t.Fatalf("with `classify_types` OFF the book is charged %v — the classifier phase cannot run, so its budget is not a bound on anything; want just the terminologist's 0.25", usd)
}
r := priced(on, "/corpus")
if usd := r.bookOnceUSD(); usd != 1.0 {
t.Fatalf("with the classifier phase ON the charge is the sum of the two budgets the plan is trimmed against, got %v want 1.0", usd)
}
// …and it is added to the book ONCE, not smeared over the units — which is the whole reason the field
// exists rather than being folded into expected_usd per unit.
units := []editUnit{
{Members: []chunk.Chunk{{Text: strings.Repeat("蛊", 300)}}},
{Members: []chunk.Chunk{{Text: strings.Repeat("蛊", 300)}}},
}
prices, bp := r.projectBook(r.pricePlanFor(), units)
if bp == nil || len(prices) != 2 {
t.Fatal("the projection produced nothing")
}
var unitSum float64
for _, p := range prices {
unitSum += p.ExpectedUSD
}
if bp.BookOnceUSD != 1.0 {
t.Errorf("book_once_usd = %v, want 1.0", bp.BookOnceUSD)
}
if diff := bp.ExpectedUSD - (unitSum + 1.0); diff > 1e-12 || diff < -1e-12 {
t.Errorf("the book price %v is not its units %v plus the once-per-book charge 1.0 — a second unit must not pay it again", bp.ExpectedUSD, unitSum)
}
}
// TestAPipelineWithNoTranslatorStagePricesItsFirstStageAsTheTranslation pins the fallback the projection
// takes when no stage carries the translator role — a shape the loader accepts (it validates stage ROLES
// and imposes no order or count) and no other test in the tree builds.
//
// ⛔ IT NEEDS TWO DIFFERENT FALLBACKS AND HAD ONE. runStageSequence starts such a wave with an empty
// `prev`, and the first stage's global index is 0, so the executor sizes it from the SOURCE — that is its
// INPUT, and using the source's token count there is right. Its OUTPUT is a different question: this
// stage is the one doing the translating, and what the engine believes a source of this shape produces is
// the PAIR'S FERTILITY. One number for both under-states that stage's completion by the whole
// coefficient — about a sixth on the shipped pair, in the under-stating direction.
//
// The assertion is that the fertility MATTERS: same text, same stage, two calibrations, two prices.
func TestAPipelineWithNoTranslatorStagePricesItsFirstStageAsTheTranslation(t *testing.T) {
priced := func(fertCJK float64) (*Runner, *pricePlan) {
pricer, err := ledger.NewPricer(nil, ledger.ModelPrice{InputPerM: 1, OutputPerM: 1})
if err != nil {
t.Fatal(err)
}
// A stage that is NOT the translator role — so waveStagesIndexed puts nothing in the draft wave.
st := config.InternalCall{Name: "s", Role: "editor"}.Stage()
r := &Runner{
Models: &config.Models{},
Pricer: pricer,
Pipeline: &config.Pipeline{
Stages: []config.Stage{st},
Segmentation: config.Segmentation{Fertility: config.Fertility{CJK: fertCJK, Other: 0.3852}},
},
templates: map[string]*PromptTemplate{"s": {System: "S", User: "U " + sourcePlaceholder}},
}
return r, r.pricePlanFor()
}
u := editUnit{Members: []chunk.Chunk{{Text: strings.Repeat("蛊", 900)}}}
_, thin := priced(0.5)
_, fat := priced(2.0)
cheap, dear := thin.projectUnit(u), fat.projectUnit(u)
if cheap.ExpectedUSD >= dear.ExpectedUSD {
t.Fatalf("the pair's fertility does not reach the price of a translator-less pipeline (%v vs %v) — its only stage IS the translation, and its completion is being taken from the source's token count instead",
cheap.ExpectedUSD, dear.ExpectedUSD)
}
// And its INPUT is still the source: the stage reads `{{text}}`, not a draft that does not exist.
if dear.DraftTokens <= 0 {
t.Errorf("the unit reports no draft at all: %+v", dear)
}
}
// TestASidecarFromAnOlderBuildStillYieldsAPrice builds the state acceptance found (V2-3) and no test in
// the tree could reach: a manifest that is CURRENT by every check the loader makes, and that predates the
// price and the structure.
//
// ⛔ WHY IT WAS UNREACHABLE BEFORE. `price` and `structure` are ADDITIVE, so manifestVersion deliberately
// did not move for them — moving it would discard every stored sidecar and re-cut every book. A file
// written by the previous build therefore passes the version, passes selfConsistent, passes the validity
// key, and comes back as «current» carrying no price. The read path took it, returned nil, and the
// fallback that the code itself calls the invariant never ran: the book reported NO PRICE AT ALL,
// silently, on the surface a buyer's platform reads before deciding.
// TestManifestServesTheReadModelsIdentically cannot see this — it compares «this pack's sidecar» with
// «no sidecar» and never builds the older FORM.
func TestASidecarFromAnOlderBuildStillYieldsAPrice(t *testing.T) {
rec := &reqRec{}
srv := newJSONProvider(rec, draftEdit)
defer srv.Close()
bookPath := setupProjectOpts(t, srv.URL, projectOpts{source: cjkSource(2, 1400)})
r := newRunner(t, bookPath)
defer r.Close()
if _, err := r.BuildAndPersistManifest(); err != nil {
t.Fatal(err)
}
// Age the sidecar: strip exactly the two additive fields and leave everything the loader validates
// untouched — the version, the counters and the key all still describe this book, cut this way.
path := r.ManifestPath()
raw, err := os.ReadFile(path)
if err != nil {
t.Fatal(err)
}
var doc map[string]any
if err := json.Unmarshal(raw, &doc); err != nil {
t.Fatal(err)
}
delete(doc, "price")
delete(doc, "structure")
aged, err := json.MarshalIndent(doc, "", " ")
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, append(aged, '\n'), 0o644); err != nil {
t.Fatal(err)
}
// The loader must still consider it CURRENT — otherwise this test is about staleness, not about an
// older FORM, and it would pass for the wrong reason.
if m := r.loadManifest(); m == nil {
t.Fatal("fixture drifted: the aged sidecar must still validate, or this test is about a stale manifest instead of an old one")
} else if m.Price != nil || m.Structure != "" {
t.Fatalf("fixture drifted: the aged sidecar still carries the new fields (%+v / %q)", m.Price, m.Structure)
}
rep, err := r.Status(context.Background())
if err != nil {
t.Fatal(err)
}
if rep.Price == nil || rep.Price.StepMaxUSD <= 0 {
t.Fatalf("a sidecar from an older build silenced the price: %+v — the accelerator changed the ANSWER, which is the one thing the fallback exists to prevent", rep.Price)
}
if rep.Structure == "" {
t.Error("…and it silenced the cut's provenance too, so an order phrased in chapters would be offered against a cut nobody described")
}
}