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) } }