498 lines
20 KiB
Go
498 lines
20 KiB
Go
package pipeline
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"os"
|
||
"path/filepath"
|
||
"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))
|
||
}
|
||
|
||
// 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.
|
||
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
|
||
r := newRunner(t, bookPath)
|
||
defer r.Close()
|
||
|
||
for _, tc := range []struct {
|
||
name string
|
||
cu store.CheckpointUsage
|
||
wantUSD float64
|
||
wantPriced bool
|
||
}{
|
||
{
|
||
name: "usage on file is re-priced",
|
||
cu: store.CheckpointUsage{ModelRequested: "fake-model", UsageJSON: `{"PromptTokens":1000,"CachedTokens":200,"CompletionTokens":500}`, 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,
|
||
},
|
||
{
|
||
// Money is priced by the model that ANSWERED, not the one that was asked — the same
|
||
// PriceForResponse ordering settle uses, and the axis an escalation hop rides.
|
||
name: "the model that answered sets the price",
|
||
cu: store.CheckpointUsage{ModelRequested: "fake-model", ModelActual: "fake-dear", UsageJSON: `{"PromptTokens":1000,"CachedTokens":200,"CompletionTokens":500}`, 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,
|
||
},
|
||
} {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
usd, priced := r.repriceCheckpoint(tc.cu)
|
||
if !nearUSD(usd, tc.wantUSD) || priced != tc.wantPriced {
|
||
t.Fatalf("repriceCheckpoint = ($%.6f, priced=%v), want ($%.6f, priced=%v)", usd, priced, tc.wantUSD, tc.wantPriced)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// 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}},
|
||
},
|
||
}}
|
||
for _, tc := range []struct {
|
||
name string
|
||
cs store.ChunkStatus
|
||
wantUSD float64
|
||
wantFromHistory 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.
|
||
name: "a $0 row takes nothing from the orphans at its position",
|
||
cs: store.ChunkStatus{Chapter: 1, ChunkIdx: 0, Stage: "draft", 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,
|
||
},
|
||
} {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
usd, fromHistory := rp.usd(tc.cs)
|
||
if !nearUSD(usd, tc.wantUSD) || fromHistory != tc.wantFromHistory {
|
||
t.Fatalf("usd = ($%.6f, fromHistory=%v), want ($%.6f, fromHistory=%v)", usd, fromHistory, tc.wantUSD, tc.wantFromHistory)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
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)
|
||
}
|
||
}
|
||
|
||
// 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)
|
||
}
|
||
}
|