textmachine/backend/internal/lang/declinephrases_test.go

91 lines
5.2 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 lang
import (
"strings"
"testing"
)
// TestTheDeclineRegistryShipsEmpty pins ONE data decision: the engine ships with NO decline phrases, so
// the reply parser recognises the ⟦TM-NO-DST⟧ sentinel and nothing else.
//
// WHY A PIN AND NOT A FILE NOBODY LOOKS AT. A mechanism that lands switched off in DATA is invisible to
// every gate the project has — that class is written twice in the backlog (rows 433 and 435: a built,
// ratified mechanism sitting dark because a number in a yaml says so, with nothing red anywhere). The
// emptiness here is deliberate and has a price attached, so it is asserted where a later edit meets it.
//
// ⛔ THE CONDITION UNDER WHICH THIS TEST STOPS BEING RIGHT, stated so the next session does not have to
// guess: the registry ships empty because the population of prose declines has been MEASURED at zero on
// everything this project has bought (research/35 §2.0 row Б-5: 0 of 69 and 0 of 66, control 69/69 and
// 66/66) — so a phrase authored today would be a guess about wording, and a WRONG guess takes a rendering
// the book paid for out of its bank with no retry to recover it (backlog row 438). When a population IS
// measured, the row and this test change TOGETHER, and the measurement is what the change cites.
func TestTheDeclineRegistryShipsEmpty(t *testing.T) {
rows, err := parseDeclinePhrases(mustBankData("bankdata/decline-phrases.txt"))
if err != nil {
t.Fatalf("the shipped registry must parse: %v", err)
}
if len(rows) != 0 {
t.Errorf("the shipping decline registry carries %d language(s) of phrases: %v.\n"+
"That is a BEHAVIOUR change on the money path — a matched phrase drops a paid rendering out of "+
"the bank — so it lands with the measurement that justifies it, and this test changes in the "+
"same edit", len(rows), rows)
}
// The CONTROL that keeps the zero above from being a zero about the wrong thing: the file is read,
// non-empty and parseable — «no rows» is not «no file» (a missing asset panics in mustBankData) and
// not «every line is a comment the parser silently drops».
raw := mustBankData("bankdata/decline-phrases.txt")
if len(raw) < 500 || !strings.Contains(string(raw), "lang<TAB>phrase") {
t.Errorf("the registry file must be the documented one, %d bytes read", len(raw))
}
for _, target := range []string{"ru", "en", "xx", ""} {
if d := DeclinePhrasesFor(target); !d.Empty() || d.Match("не термин") {
t.Errorf("target %q must resolve to an INERT registry", target)
}
}
}
// TestTheDeclineMatchIsTheWholeFieldAndFoldsTheShapesAModelWrites pins the matcher on a synthetic
// registry — the shipping one is empty, and a matcher nobody can exercise is a matcher nobody has tested.
//
// The fold exists because the two sides are authored by different hands: the phrase by a human editing a
// file, the field by a model writing a reply. A row authored as «не термин» must still match «„Не термин.“»
// — and must NOT match a rendering that merely contains those words.
func TestTheDeclineMatchIsTheWholeFieldAndFoldsTheShapesAModelWrites(t *testing.T) {
rows, err := parseDeclinePhrases([]byte("# a synthetic registry\nru\tне термин\nru\tэто не имя\n*\tnot a term\n"))
if err != nil {
t.Fatal(err)
}
// Resolved by the SAME function production resolves with, over rows given instead of loaded — no
// global is swapped, so the emptiness test beside this one keeps measuring the shipped file and not
// this fixture's leftovers.
ru := declinePhrasesFrom(rows, "ru")
if ru.Empty() {
t.Fatal("premise broken: the synthetic registry must resolve non-empty, or every assertion below passes vacuously")
}
for _, yes := range []string{"не термин", " НЕ ТЕРМИН ", "«Не термин»", "не термин.", "„не термин!“", "это не имя"} {
if !ru.Match(yes) {
t.Errorf("a decline written %q must match the authored phrase — the model chooses the quoting, the file cannot", yes)
}
}
// ⛔ THE ARM WITH THE PRICE ON IT: a substring match would turn each of these paid renderings into a
// decline, dropping a row the book bought out of its own bank.
for _, no := range []string{"клан, а не термин родства", "Не терминал", "термин", "Фан Юань", ""} {
if ru.Match(no) {
t.Errorf("a rendering that is not the whole phrase must be KEPT: %q", no)
}
}
// The any-target rows join every target; a target's own rows do not leak into another target.
if !declinePhrasesFrom(rows, "en").Match("Not a term") {
t.Error("an any-target phrase must fire for a target with no rows of its own")
}
if declinePhrasesFrom(rows, "en").Match("не термин") {
t.Error("a ru phrase must not decline for an en book — the registry is keyed by language for a reason")
}
if !declinePhrasesFrom(rows, "RU ").Match("не термин") {
t.Error("the language key must fold case and spaces, or a book.yaml written `RU` silently gets no registry")
}
// A row that folds to nothing is refused rather than left matching every empty field.
if _, err := parseDeclinePhrases([]byte("ru\t \n")); err == nil {
t.Error("a phrase that folds to nothing must fail the parse")
}
}