package pipeline import ( "strings" "testing" ) // checkers_zh_ru_test.go: WS5 (г) — the DC1/DC2/DC6 checkers must CATCH the empirical trap positives // (ws5_checkers_verify.py §a) and stay SILENT on clean/in-register text (precision over recall). These // are the regression fixtures the plan requires (positives from exp15 §7.9 / exp14b). func TestDC1TimeUnits(t *testing.T) { cases := []struct { name, src, tgt string wantFlag bool }{ {"count-as-hours", "他闭关了三个时辰。", "Он затворился на три часа.", true}, // 三时辰=6h rendered «три часа» {"correct-conversion", "他闭关了三个时辰。", "Он затворился на шесть часов.", false}, // 6h — correct, no flag {"paraphrase-no-hours", "他闭关了三个时辰。", "Он затворился надолго.", false}, // no hours count → valid {"no-shichen", "他走了三里。", "Он прошёл три ли.", false}, {"arabic-count", "过了2个时辰。", "Прошло 2 часа.", true}, // 2时辰=4h rendered «2 часа» } for _, c := range cases { t.Run(c.name, func(t *testing.T) { n, _ := lintTimeUnits(c.src, c.tgt) if (n > 0) != c.wantFlag { t.Fatalf("lintTimeUnits(%q,%q) fired=%v, want %v", c.src, c.tgt, n > 0, c.wantFlag) } }) } } func TestDC2MagnitudeScale(t *testing.T) { cases := []struct { name, src, tgt string wantFlag bool }{ {"qianwan-as-thousands", "有千万条蛊虫。", "Там были тысячи гу-червей.", true}, // 千万=10M as «тысячи» {"qianwan-correct", "有千万条蛊虫。", "Там были десятки миллионов гу-червей.", false}, // «миллион» present → ok {"shushiwan-as-tens", "聚集了数十万人。", "Собрались десятки тысяч человек.", true}, // 数十万 as «десятки тысяч» {"shushiwan-correct", "聚集了数十万人。", "Собрались сотни тысяч человек.", false}, // «сотни тысяч» → ok // ws5 parity (R4-review MAJOR): a CORRECT rendering «сотни тысяч» SUPPRESSES the flag even when // «десятки тысяч» appears elsewhere as an unrelated quantity (the ok_re guard). {"shushiwan-okre-suppress", "聚集了数十万人。", "Здесь сотни тысяч воинов, а не десятки тысяч слуг.", false}, // ws5 parity (R4-review MINOR): the inner fire check is CASE-SENSITIVE (reference no re.I), so a // sentence-initial capitalized «Десятки тысяч» does NOT fire. {"case-sensitive-capitalized", "聚集了数十万人。", "Десятки тысяч человек собрались.", false}, {"no-magnitude", "他有三个朋友。", "У него три друга.", false}, } for _, c := range cases { t.Run(c.name, func(t *testing.T) { n, _ := lintMagnitudeScale(c.src, c.tgt) if (n > 0) != c.wantFlag { t.Fatalf("lintMagnitudeScale(%q,%q) fired=%v, want %v", c.src, c.tgt, n > 0, c.wantFlag) } }) } } func TestDC6RegisterLexicon(t *testing.T) { cases := []struct { name, tgt string wantN int }{ {"terem", "Он вошёл в высокий терем.", 1}, {"terem-inflected", "В тереме было тихо, у терема стоял страж.", 2}, // тереме + терема → 2 distinct register forms {"clean", "Он вошёл в высокий зал павильона.", 0}, {"substring-not-word", "Термин был странным.", 0}, // «терм» inside «термин» must NOT fire (whole-word) } for _, c := range cases { t.Run(c.name, func(t *testing.T) { n, det := lintRegisterLexicon(c.tgt) if n != c.wantN { t.Fatalf("lintRegisterLexicon(%q) = %d (%v), want %d", c.tgt, n, det, c.wantN) } }) } } // The DC checkers must stay SILENT on a non-zh / clean chunk (they self-gate on content), so a // non-Chinese book never accrues style flags from them. func TestDCCheckersSilentOnCleanChunk(t *testing.T) { r := runCheapGates("静かな図書館の朝。", "", "Тихое утро в библиотеке.", cheapGateConfig{}) if r.DC1TimeUnits != 0 || r.DC2Magnitude != 0 || r.DC6Register != 0 { t.Fatalf("DC checkers must be silent on a clean ja→ru chunk, got %+v", r) } } // TestDC3GenderInjection pins the DC3 gender constraint annotation on the editor block (WS5 §5(б)): a // gendered CONFIRMED term carries a gender directive; a genderless term does not (byte-identical line). func TestDC3GenderInjection(t *testing.T) { mk := func(gender string) []pickedEntry { return []pickedEntry{{entry: &memoryEntry{src: "方源", dst: "Фан Юань", status: "approved", gender: gender}, via: "方源", disp: memConfirmed}} } male := renderEditorConstraintBlock(mk("male")) if !strings.Contains(male, "方源 → «Фан Юань» (муж.") { t.Fatalf("male term must carry a masculine directive, got:\n%s", male) } hidden := renderEditorConstraintBlock(mk("hidden")) if !strings.Contains(hidden, "пол СКРЫТ") { t.Fatalf("hidden term must carry the gender-avoidance mandate, got:\n%s", hidden) } none := renderEditorConstraintBlock(mk("")) if !strings.HasSuffix(none, "«Фан Юань»") { t.Fatalf("a genderless term must have NO gender note (line ends at the dst), got:\n%s", none) } }