textmachine/backend/internal/checks/checkers_pack13_test.go

111 lines
6.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 checks
import "testing"
// TestPack13Checkers pins the pack-13 GENERAL checkers against representative inputs (no book-specific
// word lists): each defect shape fires and a clean counterpart / golden-shape Russian output stays 0
// (precision over recall). The 时辰/千万 cases confirm the pre-existing DC1/DC2 still catch the corpus shapes.
func TestPack13Checkers(t *testing.T) {
dcc := testCheckers(t)
// Percent scale (成 = tenths): a decimal-fraction rendering fires; a correct percent is suppressed.
if n, _ := dcc.lintPercentScale("море истинной ци 六成六 …", "море истинной ци — шесть десятых и шесть сотых"); n != 1 {
t.Errorf("percent: «шесть десятых и шесть сотых» should fire, got %d", n)
}
if n, _ := dcc.lintPercentScale("六成六", "море истинной ци — шесть и шесть десятых"); n != 1 {
t.Errorf("percent: «шесть и шесть десятых» should fire, got %d", n)
}
if n, _ := dcc.lintPercentScale("六成六", "заполнено на шестьдесят шесть процентов"); n != 0 {
t.Errorf("percent: a correct «процентов» rendering must be suppressed, got %d", n)
}
if n, _ := dcc.lintPercentScale("нет числа", "шесть десятых чего-то"); n != 0 {
t.Errorf("percent: no 成 in source must not fire, got %d", n)
}
// Latin residue (#6, D39.39): a leaked Latin WORD fires whether lowercase OR capitalised — in a Russian
// target a Latin proper noun/brand is itself a leak (transliterate to Cyrillic) and «BANK»/«TM-BANK» is
// contamination. Legit Latin-kept surfaces live on the per-project allowlist, NOT on a "has a capital" heuristic.
for _, fires := range []struct {
text string
want int
}{
{"открыл их again, горестно вздохнув", 1}, // lowercase prose leak
{"остаточное слово Cultivation в тексте", 1}, // Title-case leaked common noun (was invisible pre-#6)
{"маркер BANK протёк в финал", 1}, // ALL-CAPS bank-marker fragment (was invisible pre-#6)
{"Он держал в руках iPhone и MacBook", 2}, // two Latin brand tokens (allowlist territory, below)
} {
if n, det := lintLatinResidue(fires.text, nil); n != fires.want {
t.Errorf("latin: %q should fire ×%d, got %d %v", fires.text, fires.want, n, det)
}
}
for _, clean := range []string{
"ОТРЕДАКТИРОВАННЫЙ ПЕРЕВОД 5abc35ddfb65. Судзуки шёл по коридорам.", // a body-hash id (has digits)
"Классы таланта А, Б, В и Г — от высшей к низшей.", // Cyrillic class letters (not Latin)
"Глава II, раздел XII начинается.", // Roman numerals (case-insensitive exclusion)
"Судзуки шёл в Академию.", // Cyrillic proper noun
} {
if n, det := lintLatinResidue(clean, nil); n != 0 {
t.Errorf("latin: clean text must not fire (%q): %d %v", clean, n, det)
}
}
// The per-project allowlist exempts an intentional Latin surface — lowercase OR a capitalised brand
// (checked case-insensitively), the correct home for a kept brand rather than a "has-capital" blanket skip.
if n, _ := lintLatinResidue("сказал again снова", map[string]bool{"again": true}); n != 0 {
t.Errorf("latin: an allowlisted lowercase surface must not fire")
}
if n, _ := lintLatinResidue("держал iPhone и MacBook", map[string]bool{"iphone": true, "macbook": true}); n != 0 {
t.Errorf("latin: allowlisted brands (case-insensitive) must not fire")
}
// Broken word — the general «-йть» rule only (no book-specific lists): «войть» fires, valid words do not.
if n, _ := dcc.lintBrokenWord("хотел тихонько войть и закрыть"); n != 1 {
t.Errorf("broken: «войть» (-йть) should fire")
}
for _, clean := range []string{
"он решил войти и закрыть окно", // valid войти
"глава клана Гуюэ поклонился", // valid prose (no -йть)
"впереди идёт Фан Юань", // valid впереди
} {
if n, det := dcc.lintBrokenWord(clean); n != 0 {
t.Errorf("broken: clean text must not fire (%q): %d %v", clean, n, det)
}
}
// The pre-existing DC1/DC2 still catch the corpus shapes (general Chinese units/idioms).
if n, _ := dcc.lintTimeUnits("僵持了三个时辰", "прошло три часа"); n != 1 {
t.Errorf("time-unit: 三个时辰→«три часа» should fire")
}
if n, _ := dcc.lintMagnitudeScale("千万生灵", "погубил тысячи жизней"); n != 1 {
t.Errorf("magnitude: 千万→«тысячи» should fire")
}
}
// TestMagnitudeScaleEmptyProbeIsInert pins the pack-15 empty-probe guard: a probe WORD missing from the
// pair data must leave DC2's 千万 branch inert, never spurious. strings.Contains(x, "") is true for every
// x, so an empty fire word would have flagged every chunk whose source carries 千万 — a data slip reading
// as a checker bug — and an empty veto word would have silently disabled the branch.
func TestMagnitudeScaleEmptyProbeIsInert(t *testing.T) {
full := testCheckers(t)
src, bad := "他有千万家产。", "У него тысячи домов."
if n, _ := full.lintMagnitudeScale(src, bad); n == 0 {
t.Fatalf("test premise broken: the configured probe must flag this pair (got %d)", n)
}
for _, tc := range []struct {
name string
mutus func(*Checkers)
}{
{"empty fire word", func(c *Checkers) { c.qianwanFireWord = "" }},
{"empty veto word", func(c *Checkers) { c.qianwanVetoWord = "" }},
} {
t.Run(tc.name, func(t *testing.T) {
c := *testCheckers(t) // copy the compiled spec
tc.mutus(&c)
if n, det := c.lintMagnitudeScale(src, bad); n != 0 {
t.Errorf("an empty probe word must leave the branch INERT, got %d flags: %v", n, det)
}
// A clean chunk stays clean either way (no spurious fire on unrelated text).
if n, _ := c.lintMagnitudeScale("他走了三里。", "Он прошёл три ли."); n != 0 {
t.Errorf("clean chunk must not flag, got %d", n)
}
})
}
}