735 lines
33 KiB
Go
735 lines
33 KiB
Go
package pipeline
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"regexp"
|
||
"strings"
|
||
"testing"
|
||
|
||
"textmachine/backend/internal/chunk"
|
||
"textmachine/backend/internal/obs"
|
||
"textmachine/backend/internal/store"
|
||
)
|
||
|
||
// reprice_test.go: the consent number after a VENDOR PRICE CHANGE (row 181). The scenario is the one
|
||
// that made the row material — DeepSeek's table moved on 16.08.2026 and the projection was still adding
|
||
// up what the units had cost before it, so the operator consented to a figure the reservation would beat
|
||
// by multiples. Every assertion here is on money the operator is SHOWN, not on money that moves: the
|
||
// gate still refuses before any reservation, and the ledger is untouched.
|
||
|
||
func newRepricerT(t *testing.T, r *Runner) *repricer {
|
||
t.Helper()
|
||
rp, err := r.newRepricer()
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
return rp
|
||
}
|
||
|
||
// bumpModelPrices multiplies every price in the fixture models.yaml by factor — the vendor moving its
|
||
// table under a book that is already paid for.
|
||
func bumpModelPrices(t *testing.T, bookPath string, factor float64) {
|
||
t.Helper()
|
||
path := filepath.Join(filepath.Dir(bookPath), "models.yaml")
|
||
raw, err := os.ReadFile(path)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
old := "price: { input_per_m: 1.0, cached_per_m: 0.1, cache_write_per_m: 0, output_per_m: 2.0 }"
|
||
next := fmt.Sprintf("price: { input_per_m: %g, cached_per_m: %g, cache_write_per_m: 0, output_per_m: %g }",
|
||
1.0*factor, 0.1*factor, 2.0*factor)
|
||
changed := strings.ReplaceAll(string(raw), old, next)
|
||
if changed == string(raw) {
|
||
t.Fatal("setup: the fixture price line was not found in models.yaml")
|
||
}
|
||
writeFile(t, path, changed)
|
||
}
|
||
|
||
// TestRebillProjectionUsesTheCurrentPriceTable is the row-181 headline: units bought under the old table
|
||
// must be quoted at the NEW one, because that is the table the reservation and the settle will use.
|
||
func TestRebillProjectionUsesTheCurrentPriceTable(t *testing.T) {
|
||
rec := &reqRec{}
|
||
srv := newJSONProvider(rec, draftEdit)
|
||
defer srv.Close()
|
||
bookPath := setupProject(t, srv.URL)
|
||
ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()})
|
||
|
||
r1 := newRunner(t, bookPath)
|
||
if _, err := r1.TranslateBook(ctx); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
committedBefore, _, err := r1.Store.SpentUSD("test-book")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
r1.Close()
|
||
if committedBefore <= 0 {
|
||
t.Fatalf("setup: the first run must bill something, got %v", committedBefore)
|
||
}
|
||
|
||
const factor = 4.0 // DeepSeek's own move was ×3.0-4.7 per axis (D39.137)
|
||
bumpModelPrices(t, bookPath, factor)
|
||
driftPipelineVersion(t, bookPath)
|
||
|
||
r2 := newRunner(t, bookPath)
|
||
defer r2.Close()
|
||
statuses, err := r2.Store.ChunkStatusesForBook("test-book")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
chunks := chunksOf(t, r2)
|
||
proj, err := r2.projectRebill(statuses, chunks, func() ([]chunk.Chunk, error) { return chunks, nil }, newRepricerT(t, r2))
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if proj.Rows != 2 {
|
||
t.Fatalf("setup: both stages must be superseded, got %d row(s)", proj.Rows)
|
||
}
|
||
if proj.HistoricalRows != 0 {
|
||
t.Errorf("every unit here has its usage on file; %d were quoted from history instead", proj.HistoricalRows)
|
||
}
|
||
// The whole point: the projection is what the units cost TODAY, not what they cost when bought.
|
||
if want := factor * committedBefore; !nearUSD(proj.USD, want) {
|
||
t.Fatalf("re-payment projected at $%.6f but the current price table makes that work $%.6f — the operator would consent to the old table's number (row 181)",
|
||
proj.USD, want)
|
||
}
|
||
|
||
// The threshold's base moves with it, or the gate goes half-historical: 5% of a book costed in last
|
||
// season's money against an amount costed in this one.
|
||
rep, err := r2.Status(ctx)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if want := factor * committedBefore; !nearUSD(rep.ProjectedBookUSD, want) {
|
||
t.Fatalf("status projected_book_usd = $%.6f, want $%.6f at the current table", rep.ProjectedBookUSD, want)
|
||
}
|
||
if !nearUSD(rep.RebillUSD, proj.USD) {
|
||
t.Fatalf("status quotes $%.6f where the consent gate projects $%.6f — the two must be one number", rep.RebillUSD, proj.USD)
|
||
}
|
||
|
||
// And the operator-facing sentence: it must name the table it used, and must not still promise a sum
|
||
// of stored cost_usd — the most visible place the old text would have gone on lying.
|
||
r2.Resnapshot = true
|
||
_, terr := r2.TranslateBook(ctx)
|
||
if terr == nil {
|
||
t.Fatal("the re-payment is over the threshold — the run must refuse")
|
||
}
|
||
msg := terr.Error()
|
||
if !strings.Contains(msg, fmt.Sprintf("~$%.6f", proj.USD)) {
|
||
t.Errorf("the refusal must quote the re-priced amount ~$%.6f; got: %s", proj.USD, msg)
|
||
}
|
||
if strings.Contains(msg, "stored cost_usd") {
|
||
t.Errorf("the consent text still describes the amount as the sum of stored cost_usd: %s", msg)
|
||
}
|
||
if !strings.Contains(msg, "CURRENT price table") {
|
||
t.Errorf("the consent text must say what the amount is priced with; got: %s", msg)
|
||
}
|
||
// The threshold printed in that same sentence is 5% of the RE-PRICED book, not of the old one.
|
||
if !strings.Contains(msg, fmt.Sprintf("projected book cost $%.6f", rep.ProjectedBookUSD)) {
|
||
t.Errorf("the threshold's base must be the re-priced book cost $%.6f; got: %s", rep.ProjectedBookUSD, msg)
|
||
}
|
||
|
||
// Nothing was bought to learn any of this.
|
||
committedAfter, reservedAfter, err := r2.Store.SpentUSD("test-book")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if committedAfter != committedBefore || reservedAfter != 0 {
|
||
t.Errorf("a refused run must move no money: committed %v → %v, reserved %v", committedBefore, committedAfter, reservedAfter)
|
||
}
|
||
}
|
||
|
||
// driftPromptVersion moves the fixture's prompt_version from one token to another, so a test can drift
|
||
// the book a SECOND time (driftPipelineVersion only knows the first hop).
|
||
func driftPromptVersion(t *testing.T, bookPath, from, to string) {
|
||
t.Helper()
|
||
path := filepath.Join(filepath.Dir(bookPath), "pipeline.yaml")
|
||
raw, err := os.ReadFile(path)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
changed := strings.ReplaceAll(string(raw), "prompt_version: "+from, "prompt_version: "+to)
|
||
if changed == string(raw) {
|
||
t.Fatalf("setup: prompt_version %q not found in the fixture pipeline", from)
|
||
}
|
||
writeFile(t, path, changed)
|
||
}
|
||
|
||
// TestRebillProjectionAfterAReBuyQuotesOneGeneration is the end-to-end form of the regression: a book
|
||
// that has ALREADY been re-bought once (the very flow the consent gate governs) must still be quoted the
|
||
// cost of ONE re-payment, not the sum of everything it has ever been billed. Prices are never touched
|
||
// here, so the projection has to equal the historical per-generation cost exactly.
|
||
func TestRebillProjectionAfterAReBuyQuotesOneGeneration(t *testing.T) {
|
||
rec := &reqRec{}
|
||
srv := newJSONProvider(rec, draftEdit)
|
||
defer srv.Close()
|
||
bookPath := setupProject(t, srv.URL)
|
||
ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()})
|
||
|
||
r1 := newRunner(t, bookPath)
|
||
if _, err := r1.TranslateBook(ctx); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
r1.Close()
|
||
|
||
// Generation 2: drift both waves and consent to the re-payment, so the book now carries two
|
||
// checkpoints per unit while each chunk_status row records only the newer one's cost.
|
||
driftPromptVersion(t, bookPath, "v-test", "v-test-g2")
|
||
r2 := newRunner(t, bookPath)
|
||
r2.Resnapshot = true
|
||
r2.AcceptRebill = RebillConsent{Given: true}
|
||
if _, err := r2.TranslateBook(ctx); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
committed, _, err := r2.Store.SpentUSD("test-book")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
r2.Close()
|
||
if !nearUSD(committed, 4*fakeCallUSD) {
|
||
t.Fatalf("setup: two generations of draft+edit must bill 4 calls, got $%.6f", committed)
|
||
}
|
||
|
||
// Generation 3 is only PROJECTED: what would one more re-payment cost?
|
||
driftPromptVersion(t, bookPath, "v-test-g2", "v-test-g3")
|
||
r3 := newRunner(t, bookPath)
|
||
defer r3.Close()
|
||
statuses, err := r3.Store.ChunkStatusesForBook("test-book")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
chunks := chunksOf(t, r3)
|
||
proj, err := r3.projectRebill(statuses, chunks, func() ([]chunk.Chunk, error) { return chunks, nil }, newRepricerT(t, r3))
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if want := 2 * fakeCallUSD; !nearUSD(proj.USD, want) {
|
||
t.Errorf("a re-bought book projects the cost of ONE more re-payment ($%.6f), got $%.6f — the projection is counting superseded generations", want, proj.USD)
|
||
}
|
||
if proj.HistoricalRows != 0 {
|
||
t.Errorf("every row here has its usage on file; %d were quoted from history", proj.HistoricalRows)
|
||
}
|
||
rep, err := r3.Status(ctx)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if want := 2 * fakeCallUSD; !nearUSD(rep.ProjectedBookUSD, want) {
|
||
t.Errorf("projected_book_usd is what the book costs ONCE ($%.6f), got $%.6f", want, rep.ProjectedBookUSD)
|
||
}
|
||
}
|
||
|
||
// addModel appends a model to the fixture catalogue at `factor` times the fixture price.
|
||
func addModel(t *testing.T, bookPath, name string, factor float64) {
|
||
t.Helper()
|
||
path := filepath.Join(filepath.Dir(bookPath), "models.yaml")
|
||
raw, err := os.ReadFile(path)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
writeFile(t, path, string(raw)+fmt.Sprintf(`
|
||
%s:
|
||
provider: fake
|
||
price: { input_per_m: %g, cached_per_m: %g, cache_write_per_m: 0, output_per_m: %g }
|
||
`, name, 1.0*factor, 0.1*factor, 2.0*factor))
|
||
}
|
||
|
||
// setStageModel points a fixture stage at another model — the stage's ROUTING moving under rows that
|
||
// were answered by the old one (D39.150 п.1).
|
||
func setStageModel(t *testing.T, bookPath, stage, model string) {
|
||
t.Helper()
|
||
path := filepath.Join(filepath.Dir(bookPath), "pipeline.yaml")
|
||
raw, err := os.ReadFile(path)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
lines := strings.Split(string(raw), "\n")
|
||
found := false
|
||
stageModel := regexp.MustCompile(`model: [^,]+,`)
|
||
for i, l := range lines {
|
||
if !strings.Contains(l, "name: "+stage+",") {
|
||
continue
|
||
}
|
||
if !stageModel.MatchString(l) {
|
||
t.Fatalf("setup: stage %q carries no model: line: %s", stage, l)
|
||
}
|
||
lines[i], found = stageModel.ReplaceAllString(l, "model: "+model+","), true
|
||
}
|
||
if !found {
|
||
t.Fatalf("setup: stage %q not found in the fixture pipeline", stage)
|
||
}
|
||
writeFile(t, path, strings.Join(lines, "\n"))
|
||
}
|
||
|
||
// TestRepriceCheckpointFallsBackToTheBilledAmount pins the one place where re-pricing is impossible and
|
||
// the honest answer is the historical figure: the billed-decode checkpoint, which settles the
|
||
// RESERVATION ESTIMATE against a `{}` usage (stagerun.go). Re-pricing that to $0 would delete real money
|
||
// from the consent number.
|
||
//
|
||
// It also pins the MODEL axis (D39.150 п.1) at the call level: a stored call is priced at the model its
|
||
// stage resolves to TODAY and never below the answering model's price — the dearer of the two — and says
|
||
// when it did so. The stage is the fixture's `draft`; `rDear` is the same book after that stage moved to a
|
||
// model ten times the price.
|
||
func TestRepriceCheckpointFallsBackToTheBilledAmount(t *testing.T) {
|
||
rec := &reqRec{}
|
||
srv := newJSONProvider(rec, draftEdit)
|
||
defer srv.Close()
|
||
bookPath := setupProject(t, srv.URL)
|
||
addModel(t, bookPath, "fake-dear", 10.0) // a second, dearer model so "priced by which one" is decidable
|
||
// One runner at a time: the store's lock refuses a second writer on the same book, so the cases that
|
||
// need the stage moved to the dear model run after the first runner is closed (`dear` below).
|
||
const usage = `{"PromptTokens":1000,"CachedTokens":200,"CompletionTokens":500}`
|
||
for _, tc := range []struct {
|
||
name string
|
||
dear bool // the draft stage resolves to fake-dear (ten times the price) when this case runs
|
||
cu store.CheckpointUsage
|
||
wantUSD float64
|
||
wantPriced bool
|
||
wantMoved bool
|
||
}{
|
||
{
|
||
name: "usage on file is re-priced",
|
||
cu: store.CheckpointUsage{ModelRequested: "fake-model", UsageJSON: usage, CostUSD: 0.01},
|
||
wantUSD: fakeCallUSD,
|
||
wantPriced: true,
|
||
},
|
||
{
|
||
name: "billed 2xx with an unreadable body keeps its settled estimate",
|
||
cu: store.CheckpointUsage{ModelRequested: "fake-model", UsageJSON: `{}`, CostUSD: 0.0042},
|
||
wantUSD: 0.0042,
|
||
wantPriced: false,
|
||
},
|
||
{
|
||
// With no stage to resolve against, the model that ANSWERED sets the price — the same
|
||
// PriceForResponse ordering settle uses, so a canonicalised slug still finds its table.
|
||
name: "with no stage in the pipeline the model that answered sets the price",
|
||
cu: store.CheckpointUsage{ModelRequested: "fake-model", ModelActual: "fake-dear", UsageJSON: usage, CostUSD: 0.01},
|
||
wantUSD: 10 * fakeCallUSD,
|
||
wantPriced: true,
|
||
},
|
||
{
|
||
name: "a genuinely $0 row is not a gap",
|
||
cu: store.CheckpointUsage{ModelRequested: "fake-model", UsageJSON: `{}`, CostUSD: 0},
|
||
wantUSD: 0,
|
||
wantPriced: true,
|
||
},
|
||
{
|
||
name: "unparseable usage keeps its billed amount",
|
||
cu: store.CheckpointUsage{ModelRequested: "fake-model", UsageJSON: `not json`, CostUSD: 0.0007},
|
||
wantUSD: 0.0007,
|
||
wantPriced: false,
|
||
},
|
||
{
|
||
name: "a stage still on its model is priced exactly and not marked as moved",
|
||
cu: store.CheckpointUsage{Stage: "draft", ModelRequested: "fake-model", ModelActual: "fake-model", UsageJSON: usage, CostUSD: 0.01},
|
||
wantUSD: fakeCallUSD,
|
||
wantPriced: true,
|
||
},
|
||
{
|
||
// The acceptance's own measurement, one call wide: the stage now resolves to a model ten
|
||
// times the price, and the number must follow the model that WILL answer, not the one that did.
|
||
name: "a stage moved to a dearer model prices at the dearer model, disclosed",
|
||
dear: true,
|
||
cu: store.CheckpointUsage{Stage: "draft", ModelRequested: "fake-model", ModelActual: "fake-model", UsageJSON: usage, CostUSD: 0.01},
|
||
wantUSD: 10 * fakeCallUSD,
|
||
wantPriced: true,
|
||
wantMoved: true,
|
||
},
|
||
{
|
||
// The other direction rounds UP too: the answering model's price is a floor, because the token
|
||
// count is that model's and the number must never be the smaller one silently.
|
||
name: "a stage moved to a cheaper model keeps the answering model's price as its floor",
|
||
cu: store.CheckpointUsage{Stage: "draft", ModelRequested: "fake-dear", ModelActual: "fake-dear", UsageJSON: usage, CostUSD: 0.1},
|
||
wantUSD: 10 * fakeCallUSD,
|
||
wantPriced: true,
|
||
wantMoved: true,
|
||
},
|
||
{
|
||
// The stage still resolves to what was asked, but a KNOWN cheaper slug answered (a router's
|
||
// fallback, a dated slug the table lists cheaper): the re-run asks for the stage's model again, so
|
||
// its price is the bound — and no routing move is claimed, because the stage did not move.
|
||
name: "a cheaper slug that answered for an unmoved stage does not pull the price below the stage's model",
|
||
dear: true,
|
||
cu: store.CheckpointUsage{Stage: "draft", ModelRequested: "fake-dear", ModelActual: "fake-model", UsageJSON: usage, CostUSD: 0.01},
|
||
wantUSD: 10 * fakeCallUSD,
|
||
wantPriced: true,
|
||
},
|
||
{
|
||
// A $0 row with NO usage on file is the derived checkpoint — free, and free again.
|
||
name: "a derived $0 row with no usage stays $0 and is not a gap",
|
||
cu: store.CheckpointUsage{Stage: "draft", ModelRequested: "fake-model", UsageJSON: `{}`, CostUSD: 0},
|
||
wantUSD: 0,
|
||
wantPriced: true,
|
||
},
|
||
{
|
||
// The same shape once the stage MOVED: there are no tokens to price at the new model, so the
|
||
// amount stays what is known and the row is reported as one that could not be re-priced. A bare
|
||
// $0 with no caveat would be the silent under-quote.
|
||
name: "a $0 row with no usage under a moved stage is disclosed, not published as free",
|
||
dear: true,
|
||
cu: store.CheckpointUsage{Stage: "draft", ModelRequested: "fake-model", UsageJSON: `{}`, CostUSD: 0},
|
||
wantUSD: 0,
|
||
wantPriced: false,
|
||
},
|
||
{
|
||
// A stage the current pipeline does not run has no «today»: nothing will re-buy the row
|
||
// (rebill.go), so its answering price stands and no routing move is claimed.
|
||
name: "a retired stage keeps the answering model's price",
|
||
dear: true,
|
||
cu: store.CheckpointUsage{Stage: "polish", ModelRequested: "fake-model", ModelActual: "fake-model", UsageJSON: usage, CostUSD: 0.01},
|
||
wantUSD: fakeCallUSD,
|
||
wantPriced: true,
|
||
},
|
||
} {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
if tc.dear {
|
||
setStageModel(t, bookPath, "draft", "fake-dear")
|
||
} else {
|
||
setStageModel(t, bookPath, "draft", "fake-model")
|
||
}
|
||
r := newRunner(t, bookPath)
|
||
defer r.Close()
|
||
usd, priced, moved := r.repriceCheckpoint(tc.cu)
|
||
if !nearUSD(usd, tc.wantUSD) || priced != tc.wantPriced || moved != tc.wantMoved {
|
||
t.Fatalf("repriceCheckpoint = ($%.6f, priced=%v, moved=%v), want ($%.6f, priced=%v, moved=%v)",
|
||
usd, priced, moved, tc.wantUSD, tc.wantPriced, tc.wantMoved)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestAnEscalationCallIsPricedAtTheHopTheStageResolvesToNow is the routing move on the OTHER slug a
|
||
// stage carries. A hop call's tokens went to escalate_to, so «the model this stage resolves to now» is the
|
||
// current hop for that row — comparing it against the primary would call every escalated row a move and
|
||
// price it at the wrong table.
|
||
func TestAnEscalationCallIsPricedAtTheHopTheStageResolvesToNow(t *testing.T) {
|
||
rec := &reqRec{}
|
||
srv := newJSONProvider(rec, echoOrClean)
|
||
defer srv.Close()
|
||
bookPath := setupEscalationProject(t, srv.URL, 1.0, nil)
|
||
addModel(t, bookPath, "fake-dear", 10.0)
|
||
const usage = `{"PromptTokens":1000,"CachedTokens":200,"CompletionTokens":500}`
|
||
|
||
same := newRunner(t, bookPath)
|
||
// The hop still resolves to the model that answered: exact, and not a move.
|
||
if usd, _, moved := same.repriceCheckpoint(store.CheckpointUsage{Stage: "draft", ModelRequested: "fake-fallback", ModelActual: "fake-fallback", UsageJSON: usage, CostUSD: 0.01, Escalation: true}); !nearUSD(usd, fakeCallUSD) || moved {
|
||
t.Fatalf("an escalation call whose hop did not move: got ($%.6f, moved=%v), want ($%.6f, moved=false)", usd, moved, fakeCallUSD)
|
||
}
|
||
same.Close() // the store's lock admits one writer per book
|
||
|
||
// The hop moves to a dearer model: the escalated row follows the HOP, the primary rows do not.
|
||
path := filepath.Join(filepath.Dir(bookPath), "pipeline.yaml")
|
||
raw, err := os.ReadFile(path)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
changed := strings.Replace(string(raw), "escalate_to: fake-fallback", "escalate_to: fake-dear", 1)
|
||
if changed == string(raw) {
|
||
t.Fatal("setup: the fixture's escalate_to was not found")
|
||
}
|
||
writeFile(t, path, changed)
|
||
dearHop := newRunner(t, bookPath)
|
||
defer dearHop.Close()
|
||
if usd, _, moved := dearHop.repriceCheckpoint(store.CheckpointUsage{Stage: "draft", ModelRequested: "fake-fallback", ModelActual: "fake-fallback", UsageJSON: usage, CostUSD: 0.01, Escalation: true}); !nearUSD(usd, 10*fakeCallUSD) || !moved {
|
||
t.Fatalf("an escalation call under a moved hop: got ($%.6f, moved=%v), want ($%.6f, moved=true)", usd, moved, 10*fakeCallUSD)
|
||
}
|
||
if usd, _, moved := dearHop.repriceCheckpoint(store.CheckpointUsage{Stage: "draft", ModelRequested: "fake-model", ModelActual: "fake-model", UsageJSON: usage, CostUSD: 0.01}); !nearUSD(usd, fakeCallUSD) || moved {
|
||
t.Fatalf("the PRIMARY call of the same stage is untouched by a hop move: got ($%.6f, moved=%v), want ($%.6f, moved=false)", usd, moved, fakeCallUSD)
|
||
}
|
||
}
|
||
|
||
// TestRepricerCountsOnlyTheCurrentGeneration is the regression an adversarial review of this pack found.
|
||
// Checkpoints are append-only across a book's whole life and carry no snapshot: a re-bought or
|
||
// source-edited unit accumulates a checkpoint per generation, while chunk_status.cost_usd is OVERWRITTEN
|
||
// with the current generation's cost alone. Re-pricing every checkpoint of the cell therefore quoted the
|
||
// operator a number that grew by a whole extra generation with each re-payment — the failure the pack
|
||
// exists to remove, reintroduced by its own fix.
|
||
func TestRepricerCountsOnlyTheCurrentGenerationOfCheckpoints(t *testing.T) {
|
||
rp := &repricer{cells: map[chunkKey]map[string][]repricedCall{
|
||
{chapter: 1, chunkIdx: 0}: {
|
||
// Two generations, oldest first, DELIBERATELY ASYMMETRIC: an expensive first generation and a
|
||
// cheap second one, so walking the wrong end gives a different answer instead of the same one.
|
||
"draft": {{then: 0.60, now: 2.40, priced: true}, {then: 0.05, now: 0.20, priced: true}},
|
||
// One generation, two attempts.
|
||
"edit": {{then: 0.01, now: 0.04, priced: true}, {then: 0.02, now: 0.08, priced: true}},
|
||
},
|
||
// One generation whose second call settled a billed-decode estimate: no usage to re-price.
|
||
{chapter: 2, chunkIdx: 0}: {
|
||
"draft": {{then: 0.03, now: 0.12, priced: true}, {then: 0.04, now: 0.04, priced: false}},
|
||
},
|
||
// Two generations again, the newer one priced across a model move (its stage resolves elsewhere now).
|
||
{chapter: 3, chunkIdx: 0}: {
|
||
"draft": {{then: 0.60, now: 2.40, priced: true}, {then: 0.01, now: 0.10, priced: true, moved: true}},
|
||
},
|
||
// A $0 generation (a model priced at zero) after a paid one, its stage moved to a paid model since.
|
||
{chapter: 4, chunkIdx: 0}: {
|
||
"draft": {{then: 0.05, now: 0.05, priced: true}, {then: 0, now: 0.02, priced: true, moved: true}, {then: 0, now: 0.02, priced: true, moved: true}},
|
||
},
|
||
// ONE generation whose primary was FREE (a $0 local model) and whose escalation hop was PAID: the
|
||
// row's whole cost is the hop's, so the hop alone accounts for it — and the free primary, which the
|
||
// re-run will make at the stage's new paid model, sits older in the same cell.
|
||
{chapter: 5, chunkIdx: 0}: {
|
||
"draft": {{then: 0, now: 0.02, priced: true, moved: true}, {then: 0.01, now: 0.01, priced: true}},
|
||
},
|
||
}}
|
||
for _, tc := range []struct {
|
||
name string
|
||
cs store.ChunkStatus
|
||
wantUSD float64
|
||
wantFromHistory bool
|
||
wantMoved bool
|
||
}{
|
||
{
|
||
// Walking from the oldest end would answer $2.40 here — the whole inference is the direction.
|
||
name: "only the newest generation is priced",
|
||
cs: store.ChunkStatus{Chapter: 1, ChunkIdx: 0, Stage: "draft", CostUSD: 0.05},
|
||
wantUSD: 0.20,
|
||
},
|
||
{
|
||
// A config REVERT rewrites the row with an OLDER call's cost while the newer, superseded call
|
||
// stays on file. The walk then overshoots the row's money, which proves the newest calls are
|
||
// not this row's — the amount falls back to what was billed, and says so.
|
||
name: "a reverted row overshoots and is quoted as billed, disclosed",
|
||
cs: store.ChunkStatus{Chapter: 1, ChunkIdx: 0, Stage: "draft", CostUSD: 0.60},
|
||
wantUSD: 0.60,
|
||
wantFromHistory: true,
|
||
},
|
||
{
|
||
name: "every attempt of the current generation counts",
|
||
cs: store.ChunkStatus{Chapter: 1, ChunkIdx: 0, Stage: "edit", CostUSD: 0.03},
|
||
wantUSD: 0.12,
|
||
},
|
||
{
|
||
// A skipped row never reached a provider and costs $0 — even where the same position was
|
||
// billed under an earlier generation whose checkpoints are still on file. ⚠ The disposition is
|
||
// SET here rather than left empty: «skipped» is the premise of the case, and it is what tells
|
||
// a genuinely free row from one whose generation could not be established (the case below).
|
||
name: "a $0 SKIPPED row takes nothing from the orphans at its position",
|
||
cs: store.ChunkStatus{Chapter: 1, ChunkIdx: 0, Stage: "draft", CostUSD: 0, Disposition: string(DispSkipped)},
|
||
wantUSD: 0,
|
||
},
|
||
{
|
||
// The same shape WITHOUT the skip: a $0 row whose newest call on file is somebody else's. No
|
||
// walk can overshoot $0, so the revert detection above cannot see it; quoting a bare $0 would
|
||
// be a silent under-quote, so the row is reported as one that could not be re-priced.
|
||
name: "a $0 row whose generation cannot be established is disclosed, not published as free",
|
||
cs: store.ChunkStatus{Chapter: 1, ChunkIdx: 0, Stage: "draft", CostUSD: 0},
|
||
wantUSD: 0,
|
||
wantFromHistory: true,
|
||
},
|
||
{
|
||
// And the OTHER side of that guard: a $0 row with no call at all on file is genuinely free —
|
||
// there is no generation to fail to establish, so disclosing it would inflate the count of
|
||
// units the operator is told could not be re-priced with rows that cost nothing by construction.
|
||
name: "a $0 row with no call on file is free, not undisclosed",
|
||
cs: store.ChunkStatus{Chapter: 8, ChunkIdx: 0, Stage: "edit", CostUSD: 0},
|
||
wantUSD: 0,
|
||
},
|
||
{
|
||
// Money the surviving checkpoints do not account for is carried at its billed value rather
|
||
// than dropped: the conservative direction, and what the projection answered before re-pricing.
|
||
name: "unexplained money is carried as billed",
|
||
cs: store.ChunkStatus{Chapter: 1, ChunkIdx: 0, Stage: "edit", CostUSD: 0.05},
|
||
wantUSD: 0.12 + 0.02,
|
||
wantFromHistory: true,
|
||
},
|
||
{
|
||
name: "a row with no checkpoint at all keeps its billed amount",
|
||
cs: store.ChunkStatus{Chapter: 9, ChunkIdx: 0, Stage: "edit", CostUSD: 0.03},
|
||
wantUSD: 0.03,
|
||
wantFromHistory: true,
|
||
},
|
||
{
|
||
// A call carried at its billed figure (no usable usage) makes the row's quote partly old money,
|
||
// and the row must SAY so: the amount is right either way, the disclosure is what the mark is.
|
||
name: "a call that could not be re-priced marks the row as quoted from history",
|
||
cs: store.ChunkStatus{Chapter: 2, ChunkIdx: 0, Stage: "draft", CostUSD: 0.07},
|
||
wantUSD: 0.16,
|
||
wantFromHistory: true,
|
||
},
|
||
{
|
||
// A call priced across a model move makes the row an upper estimate, and the row says so.
|
||
name: "a call priced at a moved model marks the row as moved",
|
||
cs: store.ChunkStatus{Chapter: 3, ChunkIdx: 0, Stage: "draft", CostUSD: 0.01},
|
||
wantUSD: 0.10,
|
||
wantMoved: true,
|
||
},
|
||
{
|
||
// A free call inside a PAID generation must travel with it: the row's money is the hop's alone,
|
||
// so a walk that stops on money drops the free primary and the price it now carries. Quoting
|
||
// only the hop is half of what the re-run buys — the silent under-quote D39.150 forbids.
|
||
name: "a free call inside a paid generation is priced, not dropped",
|
||
cs: store.ChunkStatus{Chapter: 5, ChunkIdx: 0, Stage: "draft", CostUSD: 0.01},
|
||
wantUSD: 0.03,
|
||
wantMoved: true,
|
||
},
|
||
{
|
||
// The revert fallback quotes the BILLED amount, which is not priced at any model's current
|
||
// table — so the routing caveat does not describe it and only the old-money one does.
|
||
name: "a reverted row is quoted as billed and claims no model move",
|
||
cs: store.ChunkStatus{Chapter: 3, ChunkIdx: 0, Stage: "draft", CostUSD: 0.60},
|
||
wantUSD: 0.60,
|
||
wantFromHistory: true,
|
||
},
|
||
{
|
||
// A $0 row's generation is its trailing $0 calls, and the walk stops at the older PAID one: priced
|
||
// at the model the stage resolves to now, it is money the run WILL spend and the row says so.
|
||
name: "a $0 row whose stage moved to a paid model is priced at that model",
|
||
cs: store.ChunkStatus{Chapter: 4, ChunkIdx: 0, Stage: "draft", CostUSD: 0},
|
||
wantUSD: 0.04,
|
||
wantMoved: true,
|
||
},
|
||
} {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
usd, fromHistory, moved := rp.usd(tc.cs)
|
||
if !nearUSD(usd, tc.wantUSD) || fromHistory != tc.wantFromHistory || moved != tc.wantMoved {
|
||
t.Fatalf("usd = ($%.6f, fromHistory=%v, moved=%v), want ($%.6f, fromHistory=%v, moved=%v)",
|
||
usd, fromHistory, moved, tc.wantUSD, tc.wantFromHistory, tc.wantMoved)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func nearUSD(got, want float64) bool {
|
||
d := got - want
|
||
return d < 1e-9 && d > -1e-9
|
||
}
|
||
|
||
// TestProjectionBasisNamesWhatTheAmountIsMadeOf pins the operator-facing sentence itself: the parenthetical
|
||
// the consent is given to must say which table priced the amount, and must disclose any part of it that
|
||
// could not be re-priced instead of quietly rounding that away.
|
||
func TestProjectionBasisNamesWhatTheAmountIsMadeOf(t *testing.T) {
|
||
clean := projectionBasis(RebillProjection{Rows: 4})
|
||
if !strings.Contains(clean, "CURRENT price table") || strings.Contains(clean, "originally billed") {
|
||
t.Errorf("a fully re-priced amount names the table and claims nothing else; got: %s", clean)
|
||
}
|
||
mixed := projectionBasis(RebillProjection{Rows: 4, HistoricalRows: 1})
|
||
if !strings.Contains(mixed, "CURRENT price table") || !strings.Contains(mixed, "1 of the 4") {
|
||
t.Errorf("a partly un-re-priced amount must disclose how much of it is old money; got: %s", mixed)
|
||
}
|
||
// ⚠ AND THE SAME BOTH WAYS FOR THE MODEL-MOVE CAVEAT. A clause that prints at zero puts «0 of the 4
|
||
// unit(s) were bought from a model their stage no longer resolves to» into EVERY consent text, which
|
||
// is a caveat about nothing — and the reader who learns to skip it skips it when it means something.
|
||
// The old-money clause has had this negative pin since it was written; this one had none anywhere.
|
||
if strings.Contains(clean, "bought from a model") {
|
||
t.Errorf("no unit was priced across a model move, so the caveat must not appear at all; got: %s", clean)
|
||
}
|
||
moved := projectionBasis(RebillProjection{Rows: 4, ModelMovedRows: 2})
|
||
if !strings.Contains(moved, "2 of the 4 unit(s) were bought from a model") {
|
||
t.Errorf("units priced across a model move must be disclosed with their count; got: %s", moved)
|
||
}
|
||
if strings.Contains(moved, "originally billed") {
|
||
t.Errorf("nothing here was carried at its billed amount; the old-money caveat must stay silent: %s", moved)
|
||
}
|
||
}
|
||
|
||
// TestCheckpointsOfOneGenerationSumToTheRowsCost pins the PREMISE the generation rule stands on: within a
|
||
// single generation, a disposition row's cost_usd is exactly the sum of its own checkpoints' costs. The
|
||
// rule reads membership off that identity — walk the newest calls while they fit inside the row's money —
|
||
// so a future path that bills into cost_usd without a checkpoint, or checkpoints money the row never
|
||
// counts, would not break loudly; the projection would just start quoting the wrong number.
|
||
//
|
||
// The shapes it runs are a plain draft+edit book and an escalation hop (a different model billed under the
|
||
// SAME stage name, so both calls land in one cell). It does NOT build a retried attempt, a repair call or
|
||
// a billed-decode row — those are covered at unit level in repriceCheckpoint's own table.
|
||
func TestCheckpointsOfOneGenerationSumToTheRowsCost(t *testing.T) {
|
||
for _, tc := range []struct {
|
||
name string
|
||
setup func(t *testing.T, url string) string
|
||
reply func(body string) (string, string)
|
||
}{
|
||
{"plain draft+edit", func(t *testing.T, url string) string { return setupProject(t, url) }, draftEdit},
|
||
{"an escalation hop under the same stage name", func(t *testing.T, url string) string {
|
||
return setupEscalationProject(t, url, 1.0, nil)
|
||
}, echoOrClean},
|
||
} {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
rec := &reqRec{}
|
||
srv := newJSONProvider(rec, tc.reply)
|
||
defer srv.Close()
|
||
bookPath := tc.setup(t, srv.URL)
|
||
r := newRunner(t, bookPath)
|
||
defer r.Close()
|
||
if _, err := r.TranslateBook(obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()})); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
statuses, err := r.Store.ChunkStatusesForBook(r.Book.BookID)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(statuses) == 0 {
|
||
t.Fatal("setup: the run wrote no disposition rows")
|
||
}
|
||
usage, err := r.Store.CheckpointUsageForBook(r.Book.BookID)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
stored := map[chunkKey]map[string]float64{}
|
||
for _, cu := range usage {
|
||
k := chunkKey{cu.Chapter, cu.ChunkIdx}
|
||
if stored[k] == nil {
|
||
stored[k] = map[string]float64{}
|
||
}
|
||
stored[k][cu.Stage] += cu.CostUSD
|
||
}
|
||
rp := newRepricerT(t, r)
|
||
for _, cs := range statuses {
|
||
if cs.Disposition == string(DispSkipped) {
|
||
continue // never reached a provider, so it has no checkpoints to sum
|
||
}
|
||
if got := stored[chunkKey{cs.Chapter, cs.ChunkIdx}][cs.Stage]; !nearUSD(got, cs.CostUSD) {
|
||
t.Errorf("ch%d/chunk%d/%s: checkpoints sum to $%.6f but the row records $%.6f — the generation rule reads membership off this identity",
|
||
cs.Chapter, cs.ChunkIdx, cs.Stage, got, cs.CostUSD)
|
||
}
|
||
// And the rule consumes all of them: a residue here means the walk stopped early.
|
||
if _, fromHistory, _ := rp.usd(cs); fromHistory {
|
||
t.Errorf("ch%d/chunk%d/%s: a single-generation row must be fully re-priced, not partly carried from history",
|
||
cs.Chapter, cs.ChunkIdx, cs.Stage)
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestProjectionCountsRowsItCouldNotRePrice wires the disclosure end to end: rp.usd reporting an
|
||
// un-re-priced row must reach RebillProjection.HistoricalRows and therefore the operator's sentence.
|
||
// Without this the increment can be deleted and the whole suite stays green.
|
||
func TestProjectionCountsRowsItCouldNotRePrice(t *testing.T) {
|
||
rec := &reqRec{}
|
||
srv := newJSONProvider(rec, draftEdit)
|
||
defer srv.Close()
|
||
bookPath := setupProject(t, srv.URL)
|
||
ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()})
|
||
|
||
r1 := newRunner(t, bookPath)
|
||
if _, err := r1.TranslateBook(ctx); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
r1.Close()
|
||
driftPipelineVersion(t, bookPath)
|
||
|
||
r2 := newRunner(t, bookPath)
|
||
defer r2.Close()
|
||
statuses, err := r2.Store.ChunkStatusesForBook("test-book")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
chunks := chunksOf(t, r2)
|
||
// An EMPTY repricer is the store that lost its calls: every counted row falls back to what it was
|
||
// billed, and the projection has to say so rather than present the total as freshly re-priced.
|
||
proj, err := r2.projectRebill(statuses, chunks, func() ([]chunk.Chunk, error) { return chunks, nil }, &repricer{})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if proj.Rows == 0 {
|
||
t.Fatal("setup: the drift must supersede both stages")
|
||
}
|
||
if proj.HistoricalRows != proj.Rows {
|
||
t.Fatalf("every row was quoted from history; the projection discloses %d of %d", proj.HistoricalRows, proj.Rows)
|
||
}
|
||
if basis := projectionBasis(proj); !strings.Contains(basis, fmt.Sprintf("%d of the %d", proj.Rows, proj.Rows)) {
|
||
t.Errorf("the consent sentence must disclose the un-re-priced part; got: %s", basis)
|
||
}
|
||
}
|