textmachine/backend/internal/terminology/decline_test.go

108 lines
6.6 KiB
Go
Raw 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 terminology
import (
"strings"
"testing"
"unicode"
)
// decline_test.go: the two shapes a bank role's answer takes when it is NOT a rendering, and the two
// different places they have to land.
//
// ⛔ WHY TWO BUCKETS AND NOT ONE. «Declined» means the role was asked and said it would not render the
// term — a DECISION (§C2-7's other mode), and the next pack routes it to a second opinion. «Unanswered»
// means nothing usable came back — silence, truncation, garbage — and that routes to a re-ask of the same
// model. Folding them together would make the count a lie in both directions and the routing built on it
// arbitrary. What both must NOT be is a rendering: today a prose decline and «90» are banked and reach the
// editor as the book's canon, byte-identical to a row the owner signed.
// id is the identity normalizer these fixtures use for src keys.
func idNorm(s string) string { return s }
// TestAProseDeclineLandsInTheDeclineBucketAndNotInTheBank pins the parser branch. The REGISTRY of phrases
// is language data and lives in lang/bankdata (its fold and its shipping emptiness are pinned there); here
// the predicate is a closure, so this test says what the PARSER does with a decline it is told about, and
// nothing about which words any language declines in.
func TestAProseDeclineLandsInTheDeclineBucketAndNotInTheBank(t *testing.T) {
declines := func(dst string) bool { return strings.ToLower(strings.TrimSpace(dst)) == "не термин" }
const reply = "家族\tне термин\n方源\tФан Юань"
// PREMISE, and it is the defect this branch exists to remove: with NO registry the same answer is
// BANKED. It passes wellFormedLemma, it is not an echo, it is in the target's own script — and it
// reaches the editor as law. This assertion is the measured state of the shipping engine (the registry
// lands EMPTY), not an aspiration, and it is what a first authored phrase changes.
banked, _, _ := ParseReply(reply, []string{"家族", "方源"}, idNorm, unicode.Scripts["Cyrillic"], nil)
if banked["家族"] != "не термин" {
t.Fatalf("premise broken: with no registry the prose decline must still be the (defective) banked rendering, got %q", banked["家族"])
}
got, _, st := ParseReply(reply, []string{"家族", "方源"}, idNorm, unicode.Scripts["Cyrillic"], declines)
dst, present := got["家族"]
if !present || dst != "" {
t.Errorf("a prose decline must land where the sentinel lands — present with an EMPTY rendering (that is what makes it status:auto rather than a term nobody was asked about); got present=%v dst=%q", present, dst)
}
if st.DeclinedByPhrase != 1 {
t.Errorf("DeclinedByPhrase = %d, want 1 — the count is what tells an operator the pair's prompt lost its sentinel format", st.DeclinedByPhrase)
}
if st.Bad != 0 {
t.Errorf("a decline is not a broken line: Bad = %d, want 0 (%+v)", st.Bad, st)
}
if got["方源"] != "Фан Юань" {
t.Errorf("the rendering beside the decline must be untouched, got %q", got["方源"])
}
// ⛔ THE MATCH IS THE WHOLE FIELD, NEVER A SUBSTRING, and this is the arm that costs money if it goes:
// a false decline drops a rendering the book PAID for out of the bank, and a bank role has no retry to
// recover it (backlog row 438).
inside, _, st2 := ParseReply("家族\tклан, а не термин родства", []string{"家族"}, idNorm, unicode.Scripts["Cyrillic"], declines)
if inside["家族"] == "" {
t.Errorf("a rendering that merely CONTAINS the phrase must be kept: %+v", st2)
}
}
// TestARenderingWithNoLettersReadsAsUnansweredNotAsCanon is the other shape: an answer with no letter in
// any script. «90» is two runes, carries no bracket and no dash, so wellFormedLemma passes it; the
// answer-language screen is silent on a field with no letters by construction; and past those two it is
// stamped status:draft and injected as canon.
func TestARenderingWithNoLettersReadsAsUnansweredNotAsCanon(t *testing.T) {
// ⚠ THE PUNCTUATION LINE IS TWO RUNES ON PURPOSE. A single «…» is refused one check EARLIER, by
// wellFormedLemma's minimum length — so a one-rune fixture would go red with this branch DELETED, and
// the assertion below would be satisfied by somebody else's rule. Two dashes pass every check that
// precedes this one.
const reply = "族长\t90\n方源\tОтряд 731\n花家\t——"
// PREMISE, and it has to be stated as a CONTROL rather than as «what the parser used to do» — the check
// under test is unconditional, so the old behaviour is no longer callable. The control differs from the
// refused line in ONE rune: «Глава 90» carries a letter. If this fixture were failing for some other
// reason — a mis-split column, a normalizer, the answer-language screen — the control would be refused
// too, and every assertion below would be about a broken fixture instead of about the rule.
ctrl, _, cst := ParseReply("族长\tГлава 90", []string{"族长"}, idNorm, unicode.Scripts["Cyrillic"], nil)
if ctrl["族长"] != "Глава 90" {
t.Fatalf("premise broken: the same line with ONE letter added must be accepted, got %q (%+v)", ctrl["族长"], cst)
}
got, _, st := ParseReply(reply, []string{"族长", "方源", "花家"}, idNorm, unicode.Scripts["Cyrillic"], nil)
if _, present := got["族长"]; present {
t.Errorf("«90» must be REFUSED, and refused means ABSENT from the map: a key present with an empty rendering is the DECLINE bucket, and the role did not decline — it answered with garbage (%+v)", st)
}
if _, present := got["花家"]; present {
t.Errorf("a rendering of pure punctuation must be refused too (%+v)", st)
}
if st.NoLetters != 2 || st.Bad != 2 {
t.Errorf("NoLetters = %d, Bad = %d, want 2 and 2 (%+v)", st.NoLetters, st.Bad, st)
}
if len(st.NoLettersSamples) != 2 || !strings.Contains(strings.Join(st.NoLettersSamples, "; "), "族长 → 90") {
t.Errorf("the refused lines must be NAMED, not only counted: %v", st.NoLettersSamples)
}
// ⛔ THE CONTROL, and it is the direction of error this check declares: a DIGIT is legitimate inside a
// rendering. The test is the absence of letters, not the presence of numbers — refusing «Отряд 731»
// would cost the bank real rows.
if got["方源"] != "Отряд 731" {
t.Errorf("a rendering with letters AND digits must be kept, got %q", got["方源"])
}
// And the off-language screen keeps its own verdict: a letterless field is not «answered in another
// script», and merging the two counters would make one number answer two questions.
if st.OffLanguage != 0 {
t.Errorf("OffLanguage = %d, want 0 — a field with no letters is not a field in the wrong script (%+v)", st.OffLanguage, st)
}
}