110 lines
3.7 KiB
Go
110 lines
3.7 KiB
Go
package pipeline
|
||
|
||
import (
|
||
"context"
|
||
"testing"
|
||
|
||
"textmachine/backend/internal/store"
|
||
"textmachine/backend/internal/terminology"
|
||
)
|
||
|
||
// TestBankUnionRecoversASupersededSampling is the aggregation contract end to end. The per-chunk
|
||
// telemetry row is keyed (book, chapter, chunk) and upserted, so a chunk drafted a second time OVERWRITES
|
||
// the proposals of the first — and that channel proposes a substantially different set on every sampling,
|
||
// so the overwrite loses real coverage. The checkpoints keep every answer, so the union must hold both.
|
||
//
|
||
// The second answer is written through the ordinary money path rather than simulated in the fold, so the
|
||
// test would catch a query that misses re-drafted attempts as surely as a fold that drops them.
|
||
func TestBankUnionRecoversASupersededSampling(t *testing.T) {
|
||
rec := &reqRec{}
|
||
srv := newJSONProvider(rec, func(body string) (string, string) {
|
||
if isEditBody(body) {
|
||
return "ОТРЕДАКТИРОВАННЫЙ ПЕРЕВОД", "stop"
|
||
}
|
||
return "Фан Юань пришёл к горе Цинмао.\n" + bankBlockForMining, "stop"
|
||
})
|
||
defer srv.Close()
|
||
r := newRunner(t, setupMiningStopProject(t, srv.URL, miningStopOpts{}))
|
||
defer r.Close()
|
||
if _, err := r.TranslateBook(context.Background()); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
before, _, err := r.bankObservedForBook()
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if len(before) == 0 {
|
||
t.Fatal("setup: the first sampling must have produced proposals")
|
||
}
|
||
if got := keysOf(before); contains(got, "花家") {
|
||
t.Fatalf("setup: 花家 must not be proposed yet, got %v", got)
|
||
}
|
||
// Every proposal of this run came from one chunk, so the union must not have inflated any count.
|
||
for _, o := range before {
|
||
for _, p := range o.Proposals {
|
||
if p.Chunks != 1 {
|
||
t.Fatalf("a single sampling must count once per chunk, %s→%s has %d", o.Key, p.Dst, p.Chunks)
|
||
}
|
||
}
|
||
}
|
||
|
||
// A SECOND answer for the same chunk, as a re-purchase produces: the telemetry row still holds the
|
||
// first one, the checkpoint store holds both.
|
||
snap, _, err := r.snapshotIDForWave(waveDraft)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
job, err := r.Store.EnsureJob(r.Book.BookID, 1, "draft", snap)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
res, verdict, err := r.Store.Reserve(r.Book.BookID, 0.01, store.Ceilings{BookUSD: 100, DayUSD: 100})
|
||
if err != nil || verdict != store.ReserveOK {
|
||
t.Fatalf("reserve: %v %v", verdict, err)
|
||
}
|
||
second := "Фан Юань пришёл к горе Цинмао.\n" + bankSeparator + "\n花家\tДом Хуа\tname\n"
|
||
if err := r.Store.SettleWithCheckpoint(res, 0.01, store.Checkpoint{
|
||
RequestHash: "second-sampling", JobID: job.ID, ChunkIdx: 0, Attempt: 1,
|
||
Stage: "draft", Role: roleTranslator, ModelRequested: "fake-model", ModelActual: "fake-model",
|
||
ResponseText: second, UsageJSON: "{}", CostUSD: 0.01, FinishReason: "stop",
|
||
}, nil); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
after, _, err := r.bankObservedForBook()
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if !contains(keysOf(after), "花家") {
|
||
t.Fatalf("the union must recover the superseded sampling, got %v", keysOf(after))
|
||
}
|
||
if !contains(keysOf(after), "方源") {
|
||
t.Fatalf("the union must keep the surviving sampling too, got %v", keysOf(after))
|
||
}
|
||
// The re-drafted chunk still votes once per rendering: two answers from ONE chunk are one chunk.
|
||
for _, o := range after {
|
||
for _, p := range o.Proposals {
|
||
if p.Chunks != 1 {
|
||
t.Fatalf("a re-drafted chunk must not vote twice, %s→%s has %d", o.Key, p.Dst, p.Chunks)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
func keysOf(obs []terminology.Observed) []string {
|
||
out := make([]string, 0, len(obs))
|
||
for _, o := range obs {
|
||
out = append(out, o.Key)
|
||
}
|
||
return out
|
||
}
|
||
|
||
func contains(ss []string, want string) bool {
|
||
for _, s := range ss {
|
||
if s == want {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|