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