textmachine/backend/internal/pipeline/bankreplyparse_test.go

137 lines
6.6 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package pipeline
import (
"bytes"
"log/slog"
"strings"
"testing"
"textmachine/backend/internal/terminology"
)
// bankreplyparse_test.go: the role's answer, sorted into the three outcomes the bank has for it —
// consolidated, DECLINED, UNANSWERED — end to end, through the engine that buys it.
//
// ⛔ WHY THE TWO NON-RENDERINGS MAY NOT SHARE A BUCKET. «Declined» is a decision the role made and the
// next mechanism routes it to a second opinion; «unanswered» is silence or garbage and routes to a re-ask
// of the same model. The counters are also what an owner reads at the stop. Before this pack the third
// outcome did not exist at all on the garbage side: «90» was a rendering, stamped status:draft, injected
// into the editor as this book's canon.
// TestADeclineAndGarbageLandInDifferentBuckets runs the real role over the fixture corpus and reads the
// buckets off the result the stop reports, not off the parser.
func TestADeclineAndGarbageLandInDifferentBuckets(t *testing.T) {
var logBuf bytes.Buffer
rec := &reqRec{}
srv := newJSONProvider(rec, func(body string) (string, string) {
if isTerminologyBody(body) {
// All THREE outcomes in one reply, one term each, so every count below is attributable to a
// single line: garbage a bank cannot hold, an explicit decline, and a real rendering.
return "方源\t90\n花家\t" + terminology.NoDst + "\n青茅山\tгора Цинмао", "stop"
}
if isEditBody(body) {
t.Errorf("the edit wave ran despite the bank-mining stop")
return "ОТРЕДАКТИРОВАННЫЙ ПЕРЕВОД", "stop"
}
return "Фан Юань пришёл к горе Цинмао.\n" + bankBlockForMining, "stop"
})
defer srv.Close()
bookPath := setupMiningStopProject(t, srv.URL, miningStopOpts{terminology: true})
r := newVerifyRunner(t, bookPath)
defer r.Close()
r.Log = slog.New(slog.NewTextHandler(&logBuf, &slog.HandlerOptions{Level: slog.LevelInfo}))
_ = runToSignatureStop(t, r) // the stop is the point; its fields are read off the runner below
res := r.lastTerminology
// PREMISE: the role was actually asked about both terms. Without it every count below could be a fact
// about a batch that never happened.
if res.Batches == 0 || res.Candidates != 3 {
t.Fatalf("premise broken: the role must have been asked about all three fixture terms, one per outcome: %+v", res)
}
if res.Declined != 1 {
t.Errorf("the SENTINEL answer must count as declined exactly once: %+v", res)
}
if res.NoLetters != 1 {
t.Errorf("«90» must be counted as a letterless rendering exactly once: %+v", res)
}
if res.Unanswered != 1 {
t.Errorf("a refused garbage line leaves its term UNANSWERED (the role was asked and produced nothing a bank can hold), want 1: %+v", res)
}
if res.Consolidated != 1 {
t.Errorf("the one real rendering must be consolidated, and it is the control that keeps the two counts above from being a verdict about a broken batch: %+v", res)
}
// ⛔ AND THE POINT OF ALL OF IT: «90» must not be in the document the owner signs. Before this pack it
// was there as a draft row, and from there it reaches the editor as law.
yamlMap := readSignatureMap(t, r)
if strings.Contains(yamlMap, "dst: 90") {
t.Errorf("a letterless answer must never reach the signature map as a rendering:\n%s", yamlMap)
}
if strings.Contains(yamlMap, "dst: ⟦") {
t.Errorf("the sentinel itself must never reach the signature map as a rendering:\n%s", yamlMap)
}
// The operator is told, and told WHICH line: a count alone cannot tell a truncated reply from a model
// answering in numbers.
out := logBuf.String()
for _, want := range []string{"NO LETTERS", "方源 → 90"} {
if !strings.Contains(out, want) {
t.Errorf("the refused letterless line must be named in the log, missing %q:\n%s", want, out)
}
}
}
// TestAProseDeclineNeverBecomesTheBooksCanon is the end-to-end half: the same engine, a role that declines
// IN WORDS, and a bank that must not carry those words as a rendering.
//
// ⚠ THE REGISTRY IS OVERRIDDEN HERE, and that is a statement about the shipping state rather than a
// convenience: the engine ships with an EMPTY registry (lang: TestTheDeclineRegistryShipsEmpty), so no
// fixture built on the shipped data could distinguish this mechanism from its absence. What ships is the
// wiring plus a no-op registry; what is pinned below is what the wiring DOES once a phrase exists.
func TestAProseDeclineNeverBecomesTheBooksCanon(t *testing.T) {
rec := &reqRec{}
srv := newJSONProvider(rec, func(body string) (string, string) {
if isTerminologyBody(body) {
return "方源\tне термин\n花家\tклан Хуа\n青茅山\tгора Цинмао", "stop"
}
if isEditBody(body) {
t.Errorf("the edit wave ran despite the bank-mining stop")
return "ОТРЕДАКТИРОВАННЫЙ ПЕРЕВОД", "stop"
}
return "Фан Юань пришёл к горе Цинмао.\n" + bankBlockForMining, "stop"
})
defer srv.Close()
bookPath := setupMiningStopProject(t, srv.URL, miningStopOpts{terminology: true})
// ⛔ THE WIRING ITSELF, asserted before it is overridden: an open runner always carries the predicate,
// because «the registry is empty» and «the role was never given a registry» are the same behaviour and
// must not be the same state. This is the only assertion that can see that difference while the
// shipped registry has no rows.
wired := newVerifyRunner(t, bookPath)
if wired.declinedByPhrase == nil {
t.Fatal("an open runner must carry the target's decline registry, even when that registry is empty")
}
if wired.declinedByPhrase("не термин") {
t.Error("the SHIPPING registry must decline nothing — a phrase that fires today was authored without a measured population")
}
wired.Close()
r := newVerifyRunner(t, bookPath)
defer r.Close()
r.declinedByPhrase = func(dst string) bool { return strings.TrimSpace(strings.ToLower(dst)) == "не термин" }
_ = runToSignatureStop(t, r) // the stop is the point; its fields are read off the runner below
res := r.lastTerminology
if res.DeclinedByPhrase != 1 || res.Declined != 1 {
t.Errorf("a prose decline must be counted as a decline, and named as prose: %+v", res)
}
if res.Consolidated != 2 {
t.Errorf("the two real renderings beside it must be untouched — they are the control that keeps the count above from being a verdict about a broken batch: %+v", res)
}
yamlMap := readSignatureMap(t, r)
if strings.Contains(yamlMap, "не термин") {
t.Errorf("the words of a decline must never reach the document the owner signs:\n%s", yamlMap)
}
if !strings.Contains(yamlMap, "клан Хуа") {
t.Errorf("premise broken: the renderings beside the decline must be in the map:\n%s", yamlMap)
}
}