134 lines
7.6 KiB
Go
134 lines
7.6 KiB
Go
package checks
|
||
|
||
// checkers_generality_test.go: mutation tests for the string-79 generality fixes (D39.39). They prove the
|
||
// engine no longer hardcodes the SOURCE side of the magnitude checker to a CJK source — the same behaviour
|
||
// is gated by the DECLARED source script (data), so a pair whose source is NOT in the repo behaves correctly
|
||
// without a Go change. $0, no corpus needed.
|
||
import (
|
||
"testing"
|
||
|
||
"textmachine/backend/internal/lang"
|
||
)
|
||
|
||
func TestNumberMagnitudeSourceScriptGate(t *testing.T) {
|
||
pack, err := lang.Load("../../configs/langpacks", "zh", "ru")
|
||
if err != nil {
|
||
t.Fatalf("load pack: %v", err)
|
||
}
|
||
c := CompileCheckersFor(pack, lang.TargetChecksFor("ru"))
|
||
|
||
// 三万 (3·10^4) rendered as «три миллиона» (3·10^6) is the documented 100× magnitude error.
|
||
source, final := "杀了三万人", "убил три миллиона людей"
|
||
|
||
// Declared Han source → the checker is active and fires.
|
||
c.SetSourceScripts(lang.LangScripts("zh"))
|
||
if n, _ := c.lintNumberMagnitude(source, final); n == 0 {
|
||
t.Fatalf("zh (Han) source: the magnitude error must fire, got 0 — test inputs no longer trigger")
|
||
}
|
||
|
||
// Declared Latin source (a pair NOT in the repo) → inert BY DATA, no Go change: a stray Han magnitude
|
||
// quoted inside a non-CJK source is not a magnitude the checker should judge.
|
||
c.SetSourceScripts(lang.LangScripts("en"))
|
||
if n, det := c.lintNumberMagnitude(source, final); n != 0 {
|
||
t.Errorf("en (Latin) source: magnitude checker must be inert by declared script, got %d %v", n, det)
|
||
}
|
||
|
||
// ja (Han+kana) source → dense → active again (万 is shared with Japanese).
|
||
c.SetSourceScripts(lang.LangScripts("ja"))
|
||
if n, _ := c.lintNumberMagnitude(source, final); n == 0 {
|
||
t.Errorf("ja source: dense script must keep the checker active")
|
||
}
|
||
}
|
||
|
||
// TestHomoglyphDetectorAcceptance is the deferred приёмка of the homoglyph detector (D39.64 §5.5): the
|
||
// package-6 «гy» (Cyrillic г + Latin y, the #7 case the all-Latin residue checker structurally cannot see
|
||
// because it is a MIXED-script token) is caught by the existing tk.mixed detector in detectBrokenWords. This
|
||
// closes #7 as a NON-latin-threshold concern: lowering minLatinResidueLen would not have caught it; the
|
||
// homoglyph belongs to a different, already-built detector.
|
||
func TestHomoglyphDetectorAcceptance(t *testing.T) {
|
||
pack, err := lang.Load("../../configs/langpacks", "zh", "ru")
|
||
if err != nil {
|
||
t.Fatalf("load pack: %v", err)
|
||
}
|
||
c := CompileCheckersFor(pack, lang.TargetChecksFor("ru"))
|
||
if n, det := c.detectBrokenWords("отличительная гy клана"); n == 0 {
|
||
t.Errorf("homoglyph «гy» must fire the tk.mixed detector, got 0")
|
||
} else {
|
||
t.Logf("homoglyph detector fired: %v", det)
|
||
}
|
||
// A clean Cyrillic-only word must NOT fire (precision).
|
||
if n, _ := c.detectBrokenWords("отличительная гу клана"); n != 0 {
|
||
t.Errorf("clean Cyrillic «гу» must not fire the homoglyph detector, got %d", n)
|
||
}
|
||
}
|
||
|
||
// TestInverseMarkerDashError is the mutation test for the k4_inverse build (D39.39): a dash-led line with an
|
||
// inner-speech marker is a thought typeset as spoken dialogue and fires; a plain spoken dash line does not.
|
||
func TestInverseMarkerDashError(t *testing.T) {
|
||
pack, err := lang.Load("../../configs/langpacks", "zh", "ru")
|
||
if err != nil {
|
||
t.Fatalf("load pack: %v", err)
|
||
}
|
||
c := CompileCheckersFor(pack, lang.TargetChecksFor("ru"))
|
||
cfg := CheapGateConfig{Checkers: c}
|
||
// marker in the ATTRIBUTION → fires (thought typeset as spoken).
|
||
if r := RunCheapGates("s", "d", "— Жаль этого гу, — вздохнул про себя Фан Юань.", cfg); r.DialogueDash == 0 {
|
||
t.Errorf("dash line with «про себя» in the attribution must fire the inverse-marker error")
|
||
}
|
||
// multi-clause: marker in the FIRST attribution survives a continued-speech tail.
|
||
if r := RunCheapGates("s", "d", "— Жаль, — вздохнул про себя Фан Юань. — В своё время я старался.", cfg); r.DialogueDash == 0 {
|
||
t.Errorf("attribution marker before a continued-speech clause must still fire")
|
||
}
|
||
// ADVERSARIAL FP (review find): marker inside the SPOKEN part, real attribution «сказал» → must NOT fire.
|
||
if r := RunCheapGates("s", "d", "— Повтори это про себя три раза, — сказал учитель.", cfg); r.DialogueDash != 0 {
|
||
t.Errorf("a spoken line that merely SAYS «про себя» must not fire, got %d", r.DialogueDash)
|
||
}
|
||
// ordinary spoken dash line → does not fire.
|
||
if r := RunCheapGates("s", "d", "— Здравствуй, — сказал старейшина громко.", cfg); r.DialogueDash != 0 {
|
||
t.Errorf("a plain spoken dash line must not fire, got %d", r.DialogueDash)
|
||
}
|
||
// D39.78 counterexamples: the segment-cut confines the marker test to the IMMEDIATE attribution clause.
|
||
// A marker in a following CONTINUED-SPEECH clause (after «. —») must not fire.
|
||
if r := RunCheapGates("s", "d", "— Нет, — сказал он. — Я прочту это про себя.", cfg); r.DialogueDash != 0 {
|
||
t.Errorf("«про себя» in a continued-speech clause after «. —» must not fire, got %d", r.DialogueDash)
|
||
}
|
||
// A marker in a trailing NARRATION sentence (no second dash at all) must not fire — the cut is at the mark,
|
||
// not at the next join, so a bare «. Про себя…» tail is excluded.
|
||
if r := RunCheapGates("s", "d", "— Хорошо, — кивнул он. Про себя же он выругался.", cfg); r.DialogueDash != 0 {
|
||
t.Errorf("«про себя» in a trailing narration sentence must not fire, got %d", r.DialogueDash)
|
||
}
|
||
// The homograph «про себя»=«о себе» in a clause AFTER the immediate attribution must not fire.
|
||
if r := RunCheapGates("s", "d", "— Хорошо, — сказал он, говоря про себя, а не про тебя.", cfg); r.DialogueDash != 0 {
|
||
t.Errorf("the homograph «про себя» past the immediate attribution clause must not fire, got %d", r.DialogueDash)
|
||
}
|
||
// Word boundary: «в уме» must match as a whole phrase but NOT inside «в умении».
|
||
if c.lineHasInnerMarker("сомневаясь в умении ждать") {
|
||
t.Errorf("«в уме» must not match as a substring of «в умении» (word boundary)")
|
||
}
|
||
if !c.lineHasInnerMarker("он посчитал в уме и кивнул") {
|
||
t.Errorf("«в уме» as a whole phrase must still match")
|
||
}
|
||
// data-driven generality: a target without inner_marker data leaves it inert.
|
||
bare := CompileCheckers(nil, lang.TargetChecks{})
|
||
if bare.lineHasInnerMarker("— что-то про себя") {
|
||
t.Errorf("no inner_marker data → the inverse-marker check must be inert")
|
||
}
|
||
}
|
||
|
||
// TestChevronCitationNotFlagged is the mutation test for the #3 refinement (adversarial-review find): the
|
||
// optional-comma attribution join must NOT flag a flat «X» — citation copula, even when a speech verb sits
|
||
// elsewhere on the line; an exclamatory/comma-joined reply still fires.
|
||
func TestChevronCitationNotFlagged(t *testing.T) {
|
||
// flat citation, speech verb elsewhere on the line → NOT a spoken chevron.
|
||
if chevronSpeechShape([]rune("«Бессмертный» — так его называли, с уважением произнёс старик")) {
|
||
t.Errorf("flat «X» — citation must not match chevronSpeechShape")
|
||
}
|
||
// exclamatory reply → matches.
|
||
if !chevronSpeechShape([]rune("«Гуюэ Фан Чжэн!» — крикнул старейшина")) {
|
||
t.Errorf("exclamatory «X!» — reply must match")
|
||
}
|
||
// comma-joined reply → matches.
|
||
if !chevronSpeechShape([]rune("«Реплика», — сказал он")) {
|
||
t.Errorf("comma-joined «X», — reply must match")
|
||
}
|
||
}
|