137 lines
6.6 KiB
Go
137 lines
6.6 KiB
Go
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)
|
||
}
|
||
}
|