118 lines
4.8 KiB
Go
118 lines
4.8 KiB
Go
package pipeline
|
||
|
||
import (
|
||
"context"
|
||
"strings"
|
||
"testing"
|
||
)
|
||
|
||
// repair_budget_test.go: backlog row 135 — "the ceiling gate on EVERY paid call, not on a unit of work".
|
||
//
|
||
// The ceilings themselves (book and day) were already per call: every fresh attempt reserves, and Reserve
|
||
// compares the book's cumulative committed+reserved against them (store/ledger.go, from stagerun.go). The
|
||
// gate that genuinely sat at a UNIT boundary was the repair sub-budget. It asked ONE question before the
|
||
// candidate loop — "is `spent` already at or over the budget?" — and never priced the calls it then
|
||
// authorised, so a unit that found the budget merely not-yet-exhausted could buy up to
|
||
// gates.repair.max_calls_per_unit fresh calls on the strength of that single answer, and every parallel
|
||
// worker could do the same. The number in the config was not the bound it looked like.
|
||
|
||
func TestABudgetSmallerThanOneRepairCallBuysNone(t *testing.T) {
|
||
// The clean statement of the old defect: with nothing spent yet, `spent >= budget` is false for ANY
|
||
// positive budget, so the loop admitted a call it could not afford and overshot the configured bound
|
||
// by that call's entire cost. Pricing the call is what makes the config number the bound.
|
||
rec := &reqRec{}
|
||
srv := newJSONProvider(rec, func(body string) (string, string) {
|
||
switch {
|
||
case isRepairBody(body):
|
||
return "Он прождал час и вошёл внутрь.", "stop"
|
||
case isEditBody(body):
|
||
return "Он прождал полчаса и вошёл внутрь.", "stop"
|
||
}
|
||
return "ЧЕРНОВИК ПЕРЕВОДА.", "stop"
|
||
})
|
||
defer srv.Close()
|
||
bookPath := setupRepairProject(t, srv.URL)
|
||
|
||
// What one repair call would cost, measured from the code under test rather than from a constant that
|
||
// drifts with the price table.
|
||
price := repairCallEstimate(t, bookPath)
|
||
r := newRepairRunner(t, bookPath)
|
||
defer r.Close()
|
||
r.Pipeline.Gates.Repair.BudgetUSD = price / 2 // positive, and smaller than one call
|
||
|
||
res, err := r.TranslateBook(context.Background())
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
t.Logf("one repair call reserves $%.6f; budget $%.6f — before row 135 the unit bought the call anyway (%.1f× the budget), now it buys none",
|
||
price, r.Pipeline.Gates.Repair.BudgetUSD, price/r.Pipeline.Gates.Repair.BudgetUSD)
|
||
for _, body := range rec.all() {
|
||
if isRepairBody(body) {
|
||
t.Fatalf("a repair call was bought under a budget of $%.6f that a $%.6f call cannot fit", r.Pipeline.Gates.Repair.BudgetUSD, price)
|
||
}
|
||
}
|
||
// The defect stays in the shipped text, flagged for a human — the sub-step is optional, and refusing
|
||
// it is not a failure.
|
||
if !strings.Contains(res.Chunks[0].FinalText, "полчаса") {
|
||
t.Fatalf("with the repair refused the unit must ship the editor's own bytes, got %q", res.Chunks[0].FinalText)
|
||
}
|
||
}
|
||
|
||
func TestABudgetThatFitsTheCallStillBuysIt(t *testing.T) {
|
||
// The other side of the same gate: pricing the call must not turn the sub-step off. Without this the
|
||
// test above passes for the wrong reason.
|
||
rec := &reqRec{}
|
||
srv := newJSONProvider(rec, func(body string) (string, string) {
|
||
switch {
|
||
case isRepairBody(body):
|
||
return "Он прождал час и вошёл внутрь.", "stop"
|
||
case isEditBody(body):
|
||
return "Он прождал полчаса и вошёл внутрь.", "stop"
|
||
}
|
||
return "ЧЕРНОВИК ПЕРЕВОДА.", "stop"
|
||
})
|
||
defer srv.Close()
|
||
bookPath := setupRepairProject(t, srv.URL)
|
||
|
||
price := repairCallEstimate(t, bookPath)
|
||
r := newRepairRunner(t, bookPath)
|
||
defer r.Close()
|
||
r.Pipeline.Gates.Repair.BudgetUSD = price * 1.5
|
||
|
||
res, err := r.TranslateBook(context.Background())
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
repairs := 0
|
||
for _, body := range rec.all() {
|
||
if isRepairBody(body) {
|
||
repairs++
|
||
}
|
||
}
|
||
if repairs != 1 {
|
||
t.Fatalf("a budget of $%.6f must buy the one $%.6f call, got %d", r.Pipeline.Gates.Repair.BudgetUSD, price, repairs)
|
||
}
|
||
if strings.Contains(res.Chunks[0].FinalText, "полчаса") {
|
||
t.Fatalf("the affordable repair must have applied, shipped %q", res.Chunks[0].FinalText)
|
||
}
|
||
}
|
||
|
||
// repairCallEstimate is what one repair call on this fixture would reserve — the same figure the gate
|
||
// now weighs, taken through the same helper the attempt uses.
|
||
func repairCallEstimate(t *testing.T, bookPath string) float64 {
|
||
t.Helper()
|
||
r := newRepairRunner(t, bookPath)
|
||
defer r.Close()
|
||
st := r.Pipeline.Stages[len(r.Pipeline.Stages)-1]
|
||
msgs, err := Messages(r.repairTemplates["dc1_fractional"],
|
||
RenderVars{Book: r.Book, Text: "他等了半个时辰。", Draft: "Он прождал полчаса и вошёл внутрь."})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
model, maxTokens := r.repairCallBudget(msgs)
|
||
price := r.callEstimateUSD(r.repairStage(st, model), model, msgs, maxTokens)
|
||
if price <= 0 {
|
||
t.Fatalf("a repair call must have a positive estimate, got %v", price)
|
||
}
|
||
return price
|
||
}
|