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