802 lines
38 KiB
Go
802 lines
38 KiB
Go
package pipeline
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"errors"
|
||
"fmt"
|
||
"io"
|
||
"net/http"
|
||
"net/http/httptest"
|
||
"os"
|
||
"path/filepath"
|
||
"strings"
|
||
"sync"
|
||
"testing"
|
||
|
||
"textmachine/backend/internal/chunk"
|
||
"textmachine/backend/internal/llm"
|
||
"textmachine/backend/internal/obs"
|
||
"textmachine/backend/internal/runevents"
|
||
"textmachine/backend/internal/store"
|
||
)
|
||
|
||
// retrystopmark_test.go: the acceptance battery for backlog row 291 — a USD ceiling that refuses the
|
||
// RETRY of an attempt this book ALREADY PAID FOR stops the run like any other ceiling, and the position
|
||
// it stopped over gets a MARK instead of reading as never started.
|
||
//
|
||
// Every test here is $0: the provider is an httptest server, no vendor key is touched, and the ceiling
|
||
// that drives the whole fixture is derived from the engine's OWN estimate of its own calls.
|
||
|
||
// The two chapter markers. Each fixture chapter is fixtureChapterRunes of ONE Han character, which is
|
||
// what lets the fake provider tell the chapters apart: the body carries the source text verbatim.
|
||
const (
|
||
cleanChapterRune = "文"
|
||
retriedChapterRune = "字"
|
||
)
|
||
|
||
// retryStopSource is two single-chunk chapters: the first answers cleanly, the second answers
|
||
// finish=length on its FIRST call so the attempt axis asks for a doubled budget, which is the purchase
|
||
// this fixture's ceiling refuses.
|
||
func retryStopSource() string {
|
||
return strings.Repeat(cleanChapterRune, fixtureChapterRunes) + "。\n\n" +
|
||
strings.Repeat(retriedChapterRune, fixtureChapterRunes) + "。"
|
||
}
|
||
|
||
// The two drafts the provider answers with. They differ so a test can say WHICH call's text a finished
|
||
// unit is holding — «the retry finished the unit» is otherwise indistinguishable from «attempt 0 was
|
||
// served from its checkpoint».
|
||
const (
|
||
draftTruncated = "ЧЕРНОВИК ОБОРВАН НА ПОТОЛКЕ"
|
||
draftAfterRetry = "ЧЕРНОВИК ПЕРЕВОДА ПОСЛЕ РЕТРАЯ"
|
||
)
|
||
|
||
// lengthOncePerChapter answers finish=length for the FIRST call on the retried chapter and `stop` for
|
||
// every other call, counting calls per chapter. The count is the test's only instrument for «no fresh
|
||
// paid call happened», so it is taken per chapter rather than in total.
|
||
type lengthOncePerChapter struct {
|
||
srv *httptest.Server
|
||
mu sync.Mutex
|
||
n map[string]int
|
||
}
|
||
|
||
func newLengthOncePerChapter(t *testing.T) *lengthOncePerChapter {
|
||
t.Helper()
|
||
p := &lengthOncePerChapter{n: map[string]int{}}
|
||
p.srv = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||
body, _ := io.ReadAll(r.Body)
|
||
marker := cleanChapterRune
|
||
if strings.Contains(string(body), strings.Repeat(retriedChapterRune, 8)) {
|
||
marker = retriedChapterRune
|
||
}
|
||
p.mu.Lock()
|
||
p.n[marker]++
|
||
nth := p.n[marker]
|
||
p.mu.Unlock()
|
||
text, finish := draftAfterRetry, "stop"
|
||
if marker == retriedChapterRune && nth == 1 {
|
||
// A short, varied completion: the degeneration detector must NOT claim a repetition loop
|
||
// (degenerateLoop needs 30+ words), or the verdict would be loop_degenerate — deterministic,
|
||
// NOT retryable — and this fixture would never reach the retry it is about.
|
||
text, finish = draftTruncated, "length"
|
||
}
|
||
tb, _ := json.Marshal(text)
|
||
fmt.Fprintf(w, `{"id":"fake","model":"fake-model","choices":[{"message":{"content":%s},"finish_reason":%q}],
|
||
"usage":{"prompt_tokens":1000,"completion_tokens":500,"prompt_tokens_details":{"cached_tokens":200}}}`,
|
||
tb, finish)
|
||
}))
|
||
t.Cleanup(p.srv.Close)
|
||
return p
|
||
}
|
||
|
||
func (p *lengthOncePerChapter) calls(marker string) int {
|
||
p.mu.Lock()
|
||
defer p.mu.Unlock()
|
||
return p.n[marker]
|
||
}
|
||
|
||
func (p *lengthOncePerChapter) total() int {
|
||
p.mu.Lock()
|
||
defer p.mu.Unlock()
|
||
return p.n[cleanChapterRune] + p.n[retriedChapterRune]
|
||
}
|
||
|
||
// retryStopFixture builds the project and returns it together with the manifest, asserting the SHAPE the
|
||
// whole battery depends on: TWO output units, the retried one second. A fixture that silently cut
|
||
// differently would still run — and would measure a ceiling refusing something else.
|
||
//
|
||
// ⚠ The two units are two CHUNKS of one chapter rather than two chapters, and that is measured rather
|
||
// than chosen: this fixture's source language ships no langpack, so no heading rule fires and the ingest
|
||
// reads the whole file as chapter 1 (the first version of this file asserted two chapters and said so —
|
||
// `chapters [1 1]`). It makes no difference to the subject: a draft-only pipeline makes every CHUNK its
|
||
// own output unit (outputUnits), and chunk_status is keyed per chunk.
|
||
func retryStopFixture(t *testing.T, providerURL string) (string, []chunk.Chunk) {
|
||
t.Helper()
|
||
bookPath := setupProjectOpts(t, providerURL, projectOpts{
|
||
source: retryStopSource(),
|
||
// The retry axis must be OPEN (a budget of one regeneration) — with 0 the flagged attempt is
|
||
// terminal and no reservation for a second attempt is ever asked for.
|
||
regenerate: 1,
|
||
// Draft-only: the draft IS the shipping stage, so the fixture's money is two calls plus the
|
||
// refused retry and nothing else.
|
||
draftOnly: true,
|
||
})
|
||
r := newRunner(t, bookPath)
|
||
defer r.Close()
|
||
manifest, err := r.bookChunks()
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(manifest) != 2 || manifest[0].ChunkIdx == manifest[1].ChunkIdx {
|
||
t.Fatalf("fixture must cut into 2 units, got %d chunk(s) at indices %v", len(manifest), indicesOf(manifest))
|
||
}
|
||
if !strings.Contains(manifest[1].Text, retriedChapterRune) {
|
||
t.Fatalf("the RETRIED chapter must be the second unit, or the ceiling arithmetic below is about the wrong call: chunk 1 text starts %.12q", manifest[1].Text)
|
||
}
|
||
return bookPath, manifest
|
||
}
|
||
|
||
func indicesOf(chunks []chunk.Chunk) [][2]int {
|
||
out := make([][2]int, 0, len(chunks))
|
||
for _, c := range chunks {
|
||
out = append(out, [2]int{c.Chapter, c.ChunkIdx})
|
||
}
|
||
return out
|
||
}
|
||
|
||
// retryStopCeiling is a ceiling that admits every ATTEMPT 0 of this fixture and refuses the DOUBLED
|
||
// retry. It is derived from the engine's own arithmetic — callEstimateUSD over the messages runStage
|
||
// renders and the budget maxTokensForAttempt hands it — rather than written as a literal or re-assembled
|
||
// from the same ingredients a second time, because two derivations of one number drift and then the
|
||
// fixture is quietly about a different situation.
|
||
//
|
||
// ⚠ The money the engine COMMITS per call is smaller than what it RESERVES (EstimateUSD is deliberately
|
||
// pessimistic), so the window is wide: [committed of one call + est0, committed of two + est1). Both ends
|
||
// are printed and the premise «the window exists» is asserted, because a pricing or sizing change that
|
||
// closed it would otherwise make this whole file pass while measuring nothing.
|
||
func retryStopCeiling(t *testing.T, r *Runner, manifest []chunk.Chunk) (ceiling, est0, est1 float64) {
|
||
// The retried unit is the SECOND of two, so its attempt 0 is admitted with one call's spend already
|
||
// committed and its re-attack refused with two.
|
||
return ceilingThatRefusesTheReattack(t, r, manifest[1], 1)
|
||
}
|
||
|
||
// ceilingThatRefusesTheReattack is the window for ANY fixture of this shape, in the engine's own
|
||
// arithmetic: `admitted` is how many of this fixture's calls are already committed when the position's
|
||
// attempt 0 asks for its reservation.
|
||
func ceilingThatRefusesTheReattack(t *testing.T, r *Runner, ch chunk.Chunk, admitted int) (ceiling, est0, est1 float64) {
|
||
t.Helper()
|
||
// The draft stage sizes and renders from the SOURCE; the edit stage from the draft, which is why the
|
||
// twin below takes both rather than two copies of this arithmetic existing.
|
||
return ceilingThatRefusesTheReattackAt(t, r, 0, ch, ch.Text, admitted)
|
||
}
|
||
|
||
// ceilingThatRefusesTheReattackAt is the same window for ANY stage of the pipeline: `sizingText` is what
|
||
// that stage's budget is sized from and what its template receives as the prior draft (runStage's own
|
||
// rule — the source for the translator, the previous stage's text after it).
|
||
func ceilingThatRefusesTheReattackAt(t *testing.T, r *Runner, stageIdx int, ch chunk.Chunk, sizingText string, admitted int) (ceiling, est0, est1 float64) {
|
||
t.Helper()
|
||
st := r.Pipeline.Stages[stageIdx]
|
||
vars := RenderVars{Book: r.Book, Text: ch.Text}
|
||
if stageIdx > 0 {
|
||
vars.Draft = sizingText
|
||
}
|
||
msgs, err := MessagesWithInjection(r.templates[st.Name], vars, "")
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
base := r.baseMaxTokensFor(st, EstimateTokens(sizingText))
|
||
est0 = r.callEstimateUSD(st, st.ResolvedModel, msgs, base)
|
||
est1 = r.callEstimateUSD(st, st.ResolvedModel, msgs, maxTokensForAttempt(base, 1))
|
||
lo := float64(admitted)*fakeCallUSD + est0 // this position's attempt 0 must be admitted
|
||
hi := float64(admitted+1)*fakeCallUSD + est1 // its doubled re-attack must NOT be
|
||
if lo >= hi {
|
||
t.Fatalf("no ceiling can admit attempt 0 and refuse the re-attack: lo=%.6f hi=%.6f (est0=%.6f est1=%.6f call=%.6f)", lo, hi, est0, est1, fakeCallUSD)
|
||
}
|
||
ceiling = (lo + hi) / 2
|
||
t.Logf("fixture window: est0=$%.6f est1=$%.6f committed/call=$%.6f ⇒ ceiling=$%.6f admits attempt 0 (needs ≤$%.6f) and refuses the re-attack (needs $%.6f)",
|
||
est0, est1, fakeCallUSD, ceiling, lo, hi)
|
||
return ceiling, est0, est1
|
||
}
|
||
|
||
func draftRowOf(t *testing.T, r *Runner, rows []store.ChunkStatus, ch chunk.Chunk) *store.ChunkStatus {
|
||
t.Helper()
|
||
for i := range rows {
|
||
if rows[i].Chapter == ch.Chapter && rows[i].ChunkIdx == ch.ChunkIdx && rows[i].Stage == r.Pipeline.Stages[0].Name {
|
||
return &rows[i]
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// TestTheRefusedRetryLeavesTheUnitAMarkAndNotAnEmptyPosition is pin 1 of row 291, and the measurement is
|
||
// the one the tree itself uses to describe the earlier incident of this class (stagerun.go, «THE MARK FOR
|
||
// A STOPPED POSITION IS ATTACHED TO EVERY EXIT»): money booked against a position with NO chunk_status
|
||
// row at all. Before this pack the refused retry left exactly that — committed > 0 and zero rows for the
|
||
// unit — so the unit read `pending`, indistinguishable from one nobody had started, while its first
|
||
// attempt was paid for and on disk.
|
||
func TestTheRefusedRetryLeavesTheUnitAMarkAndNotAnEmptyPosition(t *testing.T) {
|
||
prov := newLengthOncePerChapter(t)
|
||
bookPath, manifest := retryStopFixture(t, prov.srv.URL)
|
||
ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()})
|
||
|
||
r := newRunner(t, bookPath)
|
||
defer r.Close()
|
||
ceiling, _, _ := retryStopCeiling(t, r, manifest)
|
||
r.CeilingUSD = ceiling
|
||
|
||
_, err := r.TranslateBook(ctx)
|
||
var halt *CeilingHalt
|
||
if !errors.As(err, &halt) {
|
||
t.Fatalf("a ceiling that cannot afford the retry must stop the run as a ceiling halt, got %v", err)
|
||
}
|
||
// The premise, asserted and printed: the refusal fell on the RETRY, not on a fresh attempt 0. Two
|
||
// calls reached the provider — one per chapter — and the third, the retry, was never dialled.
|
||
if got, retried := prov.total(), prov.calls(retriedChapterRune); got != 2 || retried != 1 {
|
||
t.Fatalf("fixture did not reach the retry refusal: provider saw %d call(s) (%d on the retried chapter); want 2 and 1", got, retried)
|
||
}
|
||
committed, reserved, serr := r.Store.SpentUSD("test-book")
|
||
if serr != nil {
|
||
t.Fatal(serr)
|
||
}
|
||
rows, rerr := r.Store.ChunkStatusesForBook("test-book")
|
||
if rerr != nil {
|
||
t.Fatal(rerr)
|
||
}
|
||
row := draftRowOf(t, r, rows, manifest[1])
|
||
// The control величина beside the claim: the OTHER chapter's row proves the query and the store are
|
||
// answering about an existing subject, so a missing row below is a missing row and not a blind read.
|
||
clean := draftRowOf(t, r, rows, manifest[0])
|
||
t.Logf("after the stop: committed=$%.6f reserved=$%.6f · chunk_status rows=%d · row for the clean unit=%v · row for the retried unit=%v",
|
||
committed, reserved, len(rows), clean != nil, row != nil)
|
||
if committed <= 0 {
|
||
t.Fatalf("the fixture bought nothing, so there is no paid position to mark: committed=%.6f", committed)
|
||
}
|
||
if clean == nil {
|
||
t.Fatal("the clean unit has no row either — the read is blind and the assertion below would be vacuous")
|
||
}
|
||
if row == nil {
|
||
t.Fatalf("THE HOLE: the book paid $%.6f and the unit whose retry was refused has NO chunk_status row, so every reading model sees it as `pending` — not translated yet", committed)
|
||
}
|
||
if got, want := FlagReason(row.FlagReason), FlagRetryUnaffordable; got != want {
|
||
t.Fatalf("the mark must name the money as the cause, got disposition=%q reason=%q (want %q): a reason that says `length` sends a person to fix a budget formula instead of topping up",
|
||
row.Disposition, got, want)
|
||
}
|
||
if row.Disposition != string(DispFlagged) {
|
||
t.Fatalf("the mark's disposition is %q, want %q", row.Disposition, DispFlagged)
|
||
}
|
||
if row.FinalHash != "" {
|
||
t.Fatalf("the mark must not point at an export: final_hash=%q on a position that never produced shippable text", row.FinalHash)
|
||
}
|
||
if row.CostUSD <= 0 {
|
||
t.Fatalf("the mark reports cost_usd=%.6f for a position whose attempt 0 was paid for — the row would say the money went nowhere", row.CostUSD)
|
||
}
|
||
// The row has to carry what the WARN line cannot: the log dies with the process, and the row is what
|
||
// a person reads afterwards. The primary failure is the one fact FlagReason no longer states.
|
||
if !strings.Contains(row.Detail, string(FlagLength)) {
|
||
t.Fatalf("the mark's detail does not say what the paid attempt answered: %q", row.Detail)
|
||
}
|
||
if row.Attempts != 1 {
|
||
t.Fatalf("the mark counts %d attempt(s) for one paid call: a refused reservation is not an attempt that happened", row.Attempts)
|
||
}
|
||
}
|
||
|
||
// TestTheRefusedRetryStopCarriesEnoughToTopUpWith is pin 2, and it asserts the NUMBER rather than the
|
||
// presence of the frame: «an event arrived» is true of a shortfall of zero, which tells a buyer nothing.
|
||
// The sufficiency half is proved by execution in the resume test below — topping up by exactly this
|
||
// figure is what admits the same call.
|
||
func TestTheRefusedRetryStopCarriesEnoughToTopUpWith(t *testing.T) {
|
||
prov := newLengthOncePerChapter(t)
|
||
bookPath, manifest := retryStopFixture(t, prov.srv.URL)
|
||
ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()})
|
||
|
||
r := newRunner(t, bookPath)
|
||
defer r.Close()
|
||
ceiling, est0, est1 := retryStopCeiling(t, r, manifest)
|
||
r.CeilingUSD = ceiling
|
||
|
||
_, err := r.TranslateBook(ctx)
|
||
var halt *CeilingHalt
|
||
if !errors.As(err, &halt) {
|
||
t.Fatalf("want a ceiling halt, got %v", err)
|
||
}
|
||
committed, reserved, serr := r.Store.SpentUSD("test-book")
|
||
if serr != nil {
|
||
t.Fatal(serr)
|
||
}
|
||
t.Logf("stop frame: scope=%q shortfall=%d micro-USD · committed=$%.6f reserved=$%.6f ceiling=$%.6f (est0=$%.6f est1=$%.6f)",
|
||
halt.Scope, halt.ShortfallMicroUSD, committed, reserved, ceiling, est0, est1)
|
||
if halt.Scope != "book" {
|
||
t.Fatalf("the shortfall is stated for the BOOK scope only (the day ceiling sums every book in the store), and this stop says scope=%q", halt.Scope)
|
||
}
|
||
if halt.ShortfallMicroUSD <= 0 {
|
||
t.Fatal("the stop states no shortfall: the buyer is told the run paused and not by how much it is short, which is the whole of the metadata this stop is supposed to carry")
|
||
}
|
||
// The figure must be the distance to the ceiling rather than the price of the call — the one money
|
||
// number allowed to leave (D39.203). Reserved is zero here (nothing is in flight at a sequential
|
||
// refusal), so the arithmetic is exact and can be asserted rather than bounded.
|
||
want := int64((committed + est1 - ceiling) * 1e6)
|
||
if halt.ShortfallMicroUSD < want || halt.ShortfallMicroUSD > want+1 {
|
||
t.Fatalf("shortfall %d micro-USD, want %d (committed %.6f + denied estimate %.6f − ceiling %.6f, rounded up)",
|
||
halt.ShortfallMicroUSD, want, committed, est1, ceiling)
|
||
}
|
||
}
|
||
|
||
// TestToppingUpByTheStatedShortfallFinishesTheUnit is pin 3 — the three-run sequence, and the ONE pin of
|
||
// this file that is green both before and after this pack. It is a GUARD, not a proof: the resume path it
|
||
// protects is ratified (D4 — the book pauses durably and a raised ceiling resumes it), and this pack's own
|
||
// change (a mark the resume must NOT read as an answer) is exactly the kind of edit that can break it
|
||
// silently. Stated here so nobody reads its green as evidence the hole was closed; pin 1 above is what
|
||
// reds on the defect.
|
||
func TestToppingUpByTheStatedShortfallFinishesTheUnit(t *testing.T) {
|
||
prov := newLengthOncePerChapter(t)
|
||
bookPath, manifest := retryStopFixture(t, prov.srv.URL)
|
||
ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()})
|
||
|
||
// RUN 1 — stops on the refused retry.
|
||
r1 := newRunner(t, bookPath)
|
||
ceiling, _, _ := retryStopCeiling(t, r1, manifest)
|
||
r1.CeilingUSD = ceiling
|
||
_, err := r1.TranslateBook(ctx)
|
||
var halt *CeilingHalt
|
||
if !errors.As(err, &halt) {
|
||
r1.Close()
|
||
t.Fatalf("run 1 must stop on the ceiling, got %v", err)
|
||
}
|
||
shortfall := halt.ShortfallMicroUSD
|
||
callsAfterRun1 := prov.total()
|
||
r1.Close()
|
||
|
||
// RUN 2 — the same ceiling. The position is re-attacked (its mark is not an answer), attempt 0 is
|
||
// replayed from its checkpoint for $0, and the retry is refused again: no fresh paid call, no endless
|
||
// loop, the same stop.
|
||
r2 := newRunner(t, bookPath)
|
||
r2.CeilingUSD = ceiling
|
||
_, err = r2.TranslateBook(ctx)
|
||
var halt2 *CeilingHalt
|
||
if !errors.As(err, &halt2) {
|
||
r2.Close()
|
||
t.Fatalf("run 2 under the same ceiling must stop the same way, got %v", err)
|
||
}
|
||
if got := prov.total(); got != callsAfterRun1 {
|
||
r2.Close()
|
||
t.Fatalf("the resume bought %d fresh call(s) under a ceiling that has not moved (was %d, now %d): a position whose money ran out must replay, not re-buy",
|
||
got-callsAfterRun1, callsAfterRun1, got)
|
||
}
|
||
// ⛔ AND THE RE-WRITTEN MARK MUST STILL CARRY THE MONEY. The resume's fresh spend is zero — attempt 0
|
||
// came from its checkpoint — so a mark built from «what this run paid» would overwrite the row with
|
||
// cost_usd=0 and put chunk_status below SUM(checkpoints) for the position, which is the ledger
|
||
// invariant the projection a person decides on is read through.
|
||
rows2, r2err := r2.Store.ChunkStatusesForBook("test-book")
|
||
if r2err != nil {
|
||
r2.Close()
|
||
t.Fatal(r2err)
|
||
}
|
||
row2 := draftRowOf(t, r2, rows2, manifest[1])
|
||
if row2 == nil || FlagReason(row2.FlagReason) != FlagRetryUnaffordable || row2.CostUSD <= 0 {
|
||
r2.Close()
|
||
t.Fatalf("after a resume that bought nothing the mark reads %+v: the row must still name the money reason AND the money already spent on this position", row2)
|
||
}
|
||
t.Logf("after the resume under the same ceiling: fresh provider calls=%d · the mark still reads reason=%q cost_usd=%.6f attempts=%d",
|
||
prov.total()-callsAfterRun1, row2.FlagReason, row2.CostUSD, row2.Attempts)
|
||
r2.Close()
|
||
|
||
// RUN 3 — the ceiling raised by EXACTLY the shortfall the stop stated. This is the sufficiency half
|
||
// of pin 2: not «some bigger number works», but «the number the buyer was given works».
|
||
raised := ceiling + float64(shortfall)/1e6
|
||
r3 := newRunner(t, bookPath)
|
||
defer r3.Close()
|
||
r3.CeilingUSD = raised
|
||
res, err := r3.TranslateBook(ctx)
|
||
if err != nil {
|
||
t.Fatalf("topping up by the stated shortfall ($%.6f → $%.6f, +%d micro-USD) did not let the run finish: %v", ceiling, raised, shortfall, err)
|
||
}
|
||
rows, rerr := r3.Store.ChunkStatusesForBook("test-book")
|
||
if rerr != nil {
|
||
t.Fatal(rerr)
|
||
}
|
||
row := draftRowOf(t, r3, rows, manifest[1])
|
||
committed, _, _ := r3.Store.SpentUSD("test-book")
|
||
t.Logf("after the top-up: provider calls=%d (was %d) · committed=$%.6f · row=%+v", prov.total(), callsAfterRun1, committed, row)
|
||
if row == nil || row.Disposition != string(DispOK) {
|
||
t.Fatalf("the unit was not finished by the resume: row=%+v", row)
|
||
}
|
||
if got := prov.calls(retriedChapterRune); got != 2 {
|
||
t.Fatalf("the retried chapter saw %d call(s), want 2 — attempt 0 replayed for $0 and the retry bought once", got)
|
||
}
|
||
if res.Flagged != 0 {
|
||
t.Fatalf("the finished run still reports %d flagged unit(s)", res.Flagged)
|
||
}
|
||
// The delivered text must be the RETRY's, not attempt 0's: that is what «the resume finished the
|
||
// unit» means, and a checkpoint-served answer would look identical in the counters alone.
|
||
found := false
|
||
for _, c := range res.Chunks {
|
||
if c.Chapter == manifest[1].Chapter && c.ChunkIdx == manifest[1].ChunkIdx {
|
||
found = true
|
||
if !strings.Contains(c.FinalText, draftAfterRetry) {
|
||
t.Fatalf("the finished unit ships %.40q — not the retry's text", c.FinalText)
|
||
}
|
||
}
|
||
}
|
||
if !found {
|
||
t.Fatalf("the finished run carries no result for ch%d/chunk%d", manifest[1].Chapter, manifest[1].ChunkIdx)
|
||
}
|
||
}
|
||
|
||
// TestStopMarkForAsksWhetherAnythingWasEverBought is the DIRECT table over the decision, and it exists
|
||
// because the end-to-end tests above cannot reach two of its rows at all: a position that paid for a
|
||
// BURNED key and never classified a reply is left behind by a stopped run, not by a ceiling, and arranging
|
||
// one through the provider is a race where the rule is a predicate.
|
||
//
|
||
// ⛔ The row that matters most is «money was spent, nothing was classified». It is the shape a position
|
||
// has after a stopped run burned attempt 0: runAttempt walks over burned keys, so the FIRST fresh purchase
|
||
// of that position happens at an index ≥ 1 with nothing in hand. A mark written there would be a verdict
|
||
// about a unit nobody has translated once — and since a flagged row is an answer a resume serves
|
||
// (resolvedForResume), the unit would be terminal, holding the burned money and no text. The predicate
|
||
// therefore asks `paidAttempts`, and the two wrong readings of it — the attempt INDEX and «money was
|
||
// spent» — are the two rows below that must answer «no mark».
|
||
func TestStopMarkForAsksWhetherAnythingWasEverBought(t *testing.T) {
|
||
ceiling := func(shortfall int64, scope string) error {
|
||
return &CeilingHalt{Scope: scope, ShortfallMicroUSD: shortfall,
|
||
err: fmt.Errorf("pipeline: book USD ceiling reached: %w", errReserveCeiling)}
|
||
}
|
||
stopped := fmt.Errorf("the run ended: %w", context.Canceled)
|
||
delivered := &llm.AttemptCutError{Provider: "fake", Cause: llm.CutByParent, Delivered: true}
|
||
paid := stoppedPosition{
|
||
attempts: 2, paidAttempts: 1, cumCostUSD: 0.00182,
|
||
inHand: stageAttempt{attempt: 0, cls: classification{FlagLength, "truncated at max_tokens"}},
|
||
}
|
||
burnedOnly := stoppedPosition{attempts: 2, paidAttempts: 0, cumCostUSD: 0.00182}
|
||
// A position whose FIRST attempt echoed: the echo re-roll is the purchase that was refused, and the
|
||
// echo is what the row must remember — its own reason will say only why the re-roll never happened.
|
||
echoed := stoppedPosition{
|
||
attempts: 2, paidAttempts: 1, cumCostUSD: 0.00182,
|
||
inHand: stageAttempt{attempt: 0, cls: classification{FlagCJKArtifact, "CJK share 97% in output (untranslated echo)"}},
|
||
firstFlagReason: FlagCJKArtifact,
|
||
}
|
||
// A burn at index 0, a classified attempt at 1, the refusal at 2: the shape a position has when a
|
||
// stopped run left money with no result behind it and the next run bought the answer itself.
|
||
afterABurn := stoppedPosition{
|
||
attempts: 3, paidAttempts: 1, cumCostUSD: 0.00364,
|
||
inHand: stageAttempt{attempt: 1, cls: classification{FlagEmpty, "empty completion (finish=stop)"}},
|
||
}
|
||
fresh := stoppedPosition{attempts: 1, paidAttempts: 0}
|
||
|
||
for _, tc := range []struct {
|
||
name string
|
||
p stoppedPosition
|
||
err error
|
||
want FlagReason
|
||
attempts int
|
||
// wantFirst is the superseded first failure the mark must carry. It is asserted because without it
|
||
// an echo the book PAID for leaves the echo metric's numerator while staying in its denominator —
|
||
// the rate falls on the run that bought the echo (echorecovered_test.go drives that end to end).
|
||
wantFirst string
|
||
says []string
|
||
omits []string
|
||
}{
|
||
{name: "a ceiling refused the re-attack of a paid attempt", p: paid, err: ceiling(3710, "book"),
|
||
want: FlagRetryUnaffordable, attempts: 1,
|
||
says: []string{"attempt 0", string(FlagLength), "book", "3710 micro-USD", "raise the ceiling"}},
|
||
{name: "the day ceiling states no shortfall", p: paid, err: ceiling(0, "day"),
|
||
want: FlagRetryUnaffordable, attempts: 1,
|
||
says: []string{"attempt 0", "day"},
|
||
omits: []string{"short by"}},
|
||
{name: "a re-attack refused at a position that had burned a key first", p: afterABurn, err: ceiling(3710, "book"),
|
||
want: FlagRetryUnaffordable, attempts: 2,
|
||
says: []string{"attempt 1", string(FlagEmpty)}},
|
||
{name: "a re-attack refused over an echo the book had paid for", p: echoed, err: ceiling(3710, "book"),
|
||
want: FlagRetryUnaffordable, attempts: 1, wantFirst: string(FlagCJKArtifact),
|
||
says: []string{string(FlagCJKArtifact)}},
|
||
{name: "a human stopped a delivered call over a position that had already failed once", p: echoed,
|
||
err: fmt.Errorf("%w: %w", stopped, delivered),
|
||
want: FlagCancelled, attempts: 2, wantFirst: string(FlagCJKArtifact)},
|
||
{name: "a human stopped a delivered call", p: paid, err: fmt.Errorf("%w: %w", stopped, delivered),
|
||
want: FlagCancelled, attempts: 2, says: []string{"in flight", "the same budget"}},
|
||
{name: "a ceiling refused a position that only ever paid for a burned key", p: burnedOnly, err: ceiling(3710, "book")},
|
||
{name: "a ceiling refused a fresh attempt 0", p: fresh, err: ceiling(3710, "book")},
|
||
{name: "a stop with no delivered call", p: paid, err: stopped},
|
||
{name: "an ordinary infra failure", p: paid, err: errors.New("the store is gone")},
|
||
} {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
mark, marked := stopMarkFor(tc.p, tc.err)
|
||
if tc.want == "" {
|
||
if marked {
|
||
t.Fatalf("a mark was written where the position has nothing to report: %+v", mark)
|
||
}
|
||
return
|
||
}
|
||
if !marked {
|
||
t.Fatalf("no mark: the position paid $%.6f and the stop would leave it reading as never started", tc.p.cumCostUSD)
|
||
}
|
||
if mark.reason != tc.want {
|
||
t.Fatalf("reason %q, want %q", mark.reason, tc.want)
|
||
}
|
||
if mark.attempts != tc.attempts {
|
||
t.Fatalf("the mark counts %d attempt(s), want %d: a refused reservation is not an attempt that happened", mark.attempts, tc.attempts)
|
||
}
|
||
if mark.firstFlag != tc.wantFirst {
|
||
t.Fatalf("the mark carries first_flag_reason=%q, want %q: the column is how a failure the book PAID for stays countable once the row's own reason says something else",
|
||
mark.firstFlag, tc.wantFirst)
|
||
}
|
||
for _, want := range tc.says {
|
||
if !strings.Contains(mark.detail, want) {
|
||
t.Fatalf("the detail a person reads afterwards does not say %q: %s", want, mark.detail)
|
||
}
|
||
}
|
||
for _, never := range tc.omits {
|
||
if strings.Contains(mark.detail, never) {
|
||
t.Fatalf("the detail says %q about a stop that stated no figure — a money number of zero reads as «you are not short of anything»: %s", never, mark.detail)
|
||
}
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
// TestACeilingRefusingAFreshAttemptLeavesNoRow is the other side of the predicate, asserted through a run:
|
||
// a position that bought NOTHING must stay `pending`, because pending is then the TRUTH. A mark there
|
||
// would invent a half-done unit out of a unit nobody started, and it is exactly what the wrong predicate
|
||
// (the attempt index, or «money moved in this run») produces.
|
||
func TestACeilingRefusingAFreshAttemptLeavesNoRow(t *testing.T) {
|
||
prov := newLengthOncePerChapter(t)
|
||
bookPath, manifest := retryStopFixture(t, prov.srv.URL)
|
||
ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()})
|
||
|
||
r := newRunner(t, bookPath)
|
||
defer r.Close()
|
||
// A ceiling that admits nothing at all: the very first attempt 0 is refused.
|
||
r.CeilingUSD = 0.0000001
|
||
|
||
_, err := r.TranslateBook(ctx)
|
||
var halt *CeilingHalt
|
||
if !errors.As(err, &halt) {
|
||
t.Fatalf("want a ceiling halt, got %v", err)
|
||
}
|
||
if got := prov.total(); got != 0 {
|
||
t.Fatalf("premise broken: the provider saw %d call(s), so something WAS bought and this is no longer the unbought case", got)
|
||
}
|
||
committed, reserved, serr := r.Store.SpentUSD("test-book")
|
||
if serr != nil {
|
||
t.Fatal(serr)
|
||
}
|
||
rows, rerr := r.Store.ChunkStatusesForBook("test-book")
|
||
if rerr != nil {
|
||
t.Fatal(rerr)
|
||
}
|
||
t.Logf("after a stop that bought nothing: committed=$%.6f reserved=$%.6f · chunk_status rows=%d (units in the manifest: %d)",
|
||
committed, reserved, len(rows), len(manifest))
|
||
if committed != 0 {
|
||
t.Fatalf("the fixture paid $%.6f — this test is about a position that bought nothing", committed)
|
||
}
|
||
if len(rows) != 0 {
|
||
t.Fatalf("a stop that bought nothing wrote %d chunk_status row(s): %+v — a unit nobody translated must read `pending`, which is the truth, and a flag would invent a half-done unit the resume is free to re-buy", len(rows), rows)
|
||
}
|
||
// ⛔ THE CONTROL FOR A ZERO, THROUGH THE SAME QUERY. «No row for this position» and «this read cannot
|
||
// see rows at all» are the same output, and the manifest count printed above comes from a DIFFERENT
|
||
// instrument (bookChunks). So the ceiling is raised and the book run: the same call must now answer
|
||
// with rows, which is what makes the zero above a fact about the position rather than about the read.
|
||
r.CeilingUSD = 0
|
||
if _, err := r.TranslateBook(ctx); err != nil {
|
||
t.Fatalf("the control run under the book's own ceiling must finish: %v", err)
|
||
}
|
||
after, aerr := r.Store.ChunkStatusesForBook("test-book")
|
||
if aerr != nil {
|
||
t.Fatal(aerr)
|
||
}
|
||
t.Logf("control: the same query, after a run that was allowed to buy — chunk_status rows=%d (provider calls=%d)", len(after), prov.total())
|
||
if len(after) != len(manifest) {
|
||
t.Fatalf("the control read %d row(s) for %d unit(s): the zero above may have been this query's answer to everything", len(after), len(manifest))
|
||
}
|
||
}
|
||
|
||
// TestARedriveDoesNotBuyThePaidAttemptAgain is a MONEY pin, and the defect it closes was created by this
|
||
// very pack: before the mark existed, a position whose re-attack a ceiling refused had NO row, so a
|
||
// redrive could not see it. A flagged row is a redrive target by default — and ResetChunkStages DELETES
|
||
// the position's checkpoints, including the one holding the ALREADY PAID text of attempt 0. The operator's
|
||
// natural gesture after a run full of flags («tmctl redrive», no selector) would then buy that attempt
|
||
// again, while the resume needed only the re-attack.
|
||
//
|
||
// The control is in the same store and through the same query: flip the row's reason to a real verdict and
|
||
// the target appears. Without it «no targets» would pass on a selector that matches nothing at all.
|
||
func TestARedriveDoesNotBuyThePaidAttemptAgain(t *testing.T) {
|
||
prov := newLengthOncePerChapter(t)
|
||
bookPath, manifest := retryStopFixture(t, prov.srv.URL)
|
||
ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()})
|
||
|
||
r1 := newRunner(t, bookPath)
|
||
ceiling, _, _ := retryStopCeiling(t, r1, manifest)
|
||
r1.CeilingUSD = ceiling
|
||
_, err := r1.TranslateBook(ctx)
|
||
var halt *CeilingHalt
|
||
if !errors.As(err, &halt) {
|
||
r1.Close()
|
||
t.Fatalf("the run must stop on the ceiling, got %v", err)
|
||
}
|
||
r1.Close()
|
||
|
||
r2 := newRunner(t, bookPath)
|
||
defer r2.Close()
|
||
// The operator's default gesture: every flagged position, whatever the reason.
|
||
sum, _, rerr := r2.Redrive(ctx, RedriveSelector{Chapter: -1, ChunkIdx: -1, DryRun: true})
|
||
if rerr != nil {
|
||
t.Fatal(rerr)
|
||
}
|
||
st, serr := r2.Status(ctx)
|
||
if serr != nil {
|
||
t.Fatal(serr)
|
||
}
|
||
t.Logf("after the stop: redrive targets=%d · status says done=%d flagged=%d in_progress=%d pending=%d",
|
||
len(sum.Targets), st.Chapters[0].UnitsDone, st.Chapters[0].UnitsFlagged, st.Chapters[0].UnitsInProgress, st.Chapters[0].UnitsPending)
|
||
if len(sum.Targets) != 0 {
|
||
t.Fatalf("the redrive targets %+v: its reset DELETES the checkpoints of those positions, and the one behind this mark holds the paid text of attempt 0 — the resume replays it for $0 and buys only the re-attack", sum.Targets)
|
||
}
|
||
// Asking for the reason BY NAME does not change the answer: the position is not a verdict, and the
|
||
// cheap remedy (a raised ceiling and a resume) is the only one that finishes it.
|
||
byName, _, nerr := r2.Redrive(ctx, RedriveSelector{Chapter: -1, ChunkIdx: -1, Reason: string(FlagRetryUnaffordable), DryRun: true})
|
||
if nerr != nil {
|
||
t.Fatal(nerr)
|
||
}
|
||
if len(byName.Targets) != 0 {
|
||
t.Fatalf("named explicitly, the mark became a target again: %+v", byName.Targets)
|
||
}
|
||
|
||
// THE CONTROL, on the same store and through the same query: the position carries a real verdict, and
|
||
// the redrive must find it. A «no targets» that held whatever the data said would be vacuous.
|
||
row, gerr := r2.Store.GetChunkStatus("test-book", manifest[1].Chapter, manifest[1].ChunkIdx, "draft")
|
||
if gerr != nil || row == nil {
|
||
t.Fatalf("the marked row is gone: %v %v", row, gerr)
|
||
}
|
||
verdict := *row
|
||
verdict.FlagReason = string(FlagHardRefusal)
|
||
if uerr := r2.Store.UpsertChunkStatus(verdict); uerr != nil {
|
||
t.Fatal(uerr)
|
||
}
|
||
ctrl, _, cerr := r2.Redrive(ctx, RedriveSelector{Chapter: -1, ChunkIdx: -1, DryRun: true})
|
||
if cerr != nil {
|
||
t.Fatal(cerr)
|
||
}
|
||
t.Logf("control: with the SAME row carrying a real verdict, redrive targets=%d", len(ctrl.Targets))
|
||
if len(ctrl.Targets) != 1 {
|
||
t.Fatalf("the control found %d target(s) for a genuinely flagged position: the assertion above was about a selector that matches nothing", len(ctrl.Targets))
|
||
}
|
||
}
|
||
|
||
// TestAMarkedUnitIsCarriedAndNotChargedAGrantSlot is a MONEY pin over the volume classifier, and the
|
||
// defect it closes was created by this pack: the mark gave the position a row, the completeness test
|
||
// counted rows rather than ANSWERS, and a unit an earlier run had already paid to start was classified
|
||
// `rework` — which TAKES a grant slot (granted()) and is queued behind fresh book. The `--max-units` help
|
||
// text promises the opposite in so many words: «a unit an EARLIER run started and never shipped is
|
||
// finished outside the grant, so the same unit is never charged a slot twice».
|
||
func TestAMarkedUnitIsCarriedAndNotChargedAGrantSlot(t *testing.T) {
|
||
prov := newLengthOncePerChapter(t)
|
||
bookPath, manifest := retryStopFixture(t, prov.srv.URL)
|
||
ctx := obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()})
|
||
|
||
r1 := newRunner(t, bookPath)
|
||
ceiling, _, _ := retryStopCeiling(t, r1, manifest)
|
||
r1.CeilingUSD = ceiling
|
||
_, err := r1.TranslateBook(ctx)
|
||
var halt *CeilingHalt
|
||
if !errors.As(err, &halt) {
|
||
r1.Close()
|
||
t.Fatalf("the run must stop on the ceiling, got %v", err)
|
||
}
|
||
r1.Close()
|
||
|
||
r := newRunner(t, bookPath)
|
||
defer r.Close()
|
||
chunks, cerr := r.bookChunks()
|
||
if cerr != nil {
|
||
t.Fatal(cerr)
|
||
}
|
||
statuses, serr := r.Store.ChunkStatusesForBook("test-book")
|
||
if serr != nil {
|
||
t.Fatal(serr)
|
||
}
|
||
units := r.outputUnits(chunks)
|
||
class, _, kerr := r.classifyUnits(units, chunks, statuses, nil)
|
||
if kerr != nil {
|
||
t.Fatal(kerr)
|
||
}
|
||
marked := class[chunkKey{manifest[1].Chapter, manifest[1].ChunkIdx}]
|
||
clean := class[chunkKey{manifest[0].Chapter, manifest[0].ChunkIdx}]
|
||
t.Logf("volume classes: the marked unit=%d, the delivered one=%d (fresh=%d free=%d rework=%d carried=%d)",
|
||
marked, clean, unitFresh, unitFree, unitRework, unitCarried)
|
||
if marked != unitCarried {
|
||
t.Fatalf("the marked unit classifies %d, want carried (%d): `rework` charges this run's grant for a unit an earlier run already paid to start, and `fresh` would call it new book",
|
||
marked, unitCarried)
|
||
}
|
||
// The control, through the same call: the unit that really was delivered is FREE — so «carried» above
|
||
// is a statement about this unit and not what the classifier answers for everything.
|
||
if clean != unitFree {
|
||
t.Fatalf("the delivered unit classifies %d, want free (%d) — the classifier is answering something else entirely", clean, unitFree)
|
||
}
|
||
}
|
||
|
||
// TestTheWaveCountersDoNotTellTheBuyerTheBookIsDone is the MONEY WIRE pin, and it is the widest defect
|
||
// this pack created: the wave counter counted a unit as resolved once its rows EXISTED, so the mark made
|
||
// it full. Measured before the fix on this fixture — run 1 stopped with one unit marked and published
|
||
// `money{units_resolved:1,units_deferred:1}`; the resume under the SAME ceiling, which bought nothing and
|
||
// left the same hole, published `draft{done:2,total:2}` and `money{units_resolved:2,units_deferred:0}`.
|
||
// A buyer deciding whether to top up was being told the book owed nothing.
|
||
func TestTheWaveCountersDoNotTellTheBuyerTheBookIsDone(t *testing.T) {
|
||
prov := newLengthOncePerChapter(t)
|
||
bookPath, manifest := retryStopFixture(t, prov.srv.URL)
|
||
ceiling := 0.0
|
||
|
||
for run := 1; run <= 2; run++ {
|
||
r := newRunner(t, bookPath)
|
||
if run == 1 {
|
||
ceiling, _, _ = retryStopCeiling(t, r, manifest)
|
||
}
|
||
r.CeilingUSD = ceiling
|
||
_, err := r.TranslateBook(obs.WithReqInfo(context.Background(), obs.ReqInfo{TraceID: obs.NewTraceID()}))
|
||
var halt *CeilingHalt
|
||
if !errors.As(err, &halt) {
|
||
r.Close()
|
||
t.Fatalf("run %d must stop on the ceiling, got %v", run, err)
|
||
}
|
||
r.Close()
|
||
}
|
||
// Read the journal as BYTES, the way the platform tails it — not by asking the engine's structs.
|
||
raw, rerr := os.ReadFile(filepath.Join(filepath.Dir(bookPath), runevents.JournalFile))
|
||
if rerr != nil {
|
||
t.Fatal(rerr)
|
||
}
|
||
type money struct {
|
||
UnitsResolved int `json:"units_resolved"`
|
||
UnitsDeferred int `json:"units_deferred"`
|
||
}
|
||
var ledgers []money
|
||
var progress []string
|
||
for _, l := range strings.Split(strings.TrimSpace(string(raw)), "\n") {
|
||
if l == "" {
|
||
continue
|
||
}
|
||
var env struct {
|
||
Type string `json:"type"`
|
||
Data json.RawMessage `json:"data"`
|
||
}
|
||
if err := json.Unmarshal([]byte(l), &env); err != nil {
|
||
t.Fatalf("the stream is not readable NDJSON: %v (%s)", err, l)
|
||
}
|
||
switch env.Type {
|
||
case string(runevents.TypeFinished):
|
||
var f struct {
|
||
Outcome string `json:"outcome"`
|
||
Money *money `json:"money"`
|
||
}
|
||
if err := json.Unmarshal(env.Data, &f); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if f.Money != nil {
|
||
ledgers = append(ledgers, *f.Money)
|
||
}
|
||
case string(runevents.TypeProgress):
|
||
progress = append(progress, string(env.Data))
|
||
}
|
||
}
|
||
t.Logf("money ledgers published, in order: %+v", ledgers)
|
||
t.Logf("progress frames: %s", strings.Join(progress, " · "))
|
||
if len(ledgers) != 2 {
|
||
t.Fatalf("want one money ledger per stopped run, got %d — the fixture did not publish what this test reads", len(ledgers))
|
||
}
|
||
for i, l := range ledgers {
|
||
if l.UnitsResolved != 1 || l.UnitsDeferred != 1 {
|
||
t.Fatalf("run %d published money{resolved:%d deferred:%d}, want 1 and 1: the unit whose re-attack was refused is NOT resolved — nothing bought it, and telling a buyer the book owes %d units is the number they decide a top-up on",
|
||
i+1, l.UnitsResolved, l.UnitsDeferred, l.UnitsDeferred)
|
||
}
|
||
}
|
||
// And the resync channel must agree with the stream: one wave-resolved unit of two.
|
||
r := newRunner(t, bookPath)
|
||
defer r.Close()
|
||
st, serr := r.Status(context.Background())
|
||
if serr != nil {
|
||
t.Fatal(serr)
|
||
}
|
||
t.Logf("status says draft=%+v · chapter done=%d flagged=%d in_progress=%d",
|
||
st.Progress.Draft, st.Chapters[0].UnitsDone, st.Chapters[0].UnitsFlagged, st.Chapters[0].UnitsInProgress)
|
||
if st.Progress.Draft.Done != 1 || st.Progress.Draft.Total != 2 {
|
||
t.Fatalf("the resync channel says the draft wave finished %d of %d units: the platform folds this and the stream into ONE column, so they must not disagree",
|
||
st.Progress.Draft.Done, st.Progress.Draft.Total)
|
||
}
|
||
}
|