textmachine/backend/internal/pipeline/checkers_zh_ru_test.go

107 lines
5.3 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 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)
}
}