textmachine/backend/internal/pipeline/regressionguard_test.go

82 lines
4.4 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"
)
// regressionguard_test.go — the post-reflow regression guard (D38). Precision over recall: each
// flagger asserts BOTH firing on a real regression AND non-firing on a legitimate reflow.
func TestRegressionGuardLengthCollapse(t *testing.T) {
// A draft well over the min-chars floor, halved by the "reflow" → collapse fires.
draft := strings.Repeat("Судзуки медленно шёл по коридору академии магии. ", 12) // ~560 non-space chars
shortFinal := "Судзуки шёл."
if r := runRegressionGuard(draft, shortFinal); r.LengthCollapse == 0 {
t.Errorf("expected a length-collapse flag (draft≫final)")
}
// A legitimate reflow that merges lines keeps ~all the content → must NOT fire.
sameFinal := strings.Repeat("Судзуки медленно шёл по коридору академии магии. ", 11)
if r := runRegressionGuard(draft, sameFinal); r.LengthCollapse != 0 {
t.Errorf("length-collapse FALSE POSITIVE on a near-equal reflow: %v", r.Detail)
}
// A short draft below the floor must never fire (a % ratio is meaningless there).
if r := runRegressionGuard("Он ушёл.", "Он."); r.LengthCollapse != 0 {
t.Errorf("length-collapse fired below the min-chars floor: %v", r.Detail)
}
}
func TestRegressionGuardNumberDrift(t *testing.T) {
// 四成四=44% rendered as «44» in the draft, drifted to a word form in the final → 44 disappears.
if r := runRegressionGuard("У него было 44 процента запаса.", "У него было четыре десятых запаса."); r.NumberDrift == 0 {
t.Errorf("expected a number-drift flag (44 vanished)")
}
// A number APPEARING in the final that was not in the draft (hallucinated figure).
if r := runRegressionGuard("Он собрал войско.", "Он собрал войско из 3000 солдат."); r.NumberDrift == 0 {
t.Errorf("expected a number-drift flag (3000 appeared)")
}
clean := []struct{ draft, final string }{
// The multi-digit number is preserved across the reflow (44 on both sides).
{"У него было 44 процента.", "У него имелось 44 процента запаса."},
// Grouped-triple vs joined form of the SAME number must not drift (10 000 ≡ 10000).
{"Армия в 10 000 воинов.", "Армия насчитывала 10000 воинов."},
// SINGLE digits are excluded (routinely spelled out), so «5»→«пять» is not a drift.
{"У него было 5 мечей.", "У него было пять мечей."},
// No numbers at all — never fires.
{"Судзуки шёл по коридору академии магии.", "Судзуки медленно шёл по длинному коридору."},
}
for _, c := range clean {
if r := runRegressionGuard(c.draft, c.final); r.NumberDrift != 0 {
t.Errorf("number-drift FALSE POSITIVE on %q→%q: %v", c.draft, c.final, r.Detail)
}
}
}
// TestRunCheapGatesRegressionOptIn pins that the guard is OFF by default and folds into the
// observability total only when enabled (a book that does not opt in serialises identically).
func TestRunCheapGatesRegressionOptIn(t *testing.T) {
draft := strings.Repeat("Судзуки медленно шёл по коридору академии магии. ", 12)
final := "Судзуки шёл."
off := runCheapGates("", draft, final, cheapGateConfig{yoPolicy: "auto", allowlist: map[string]bool{}})
if off.LengthCollapse != 0 || off.NumberDrift != 0 {
t.Errorf("regression guard fired while disabled: %+v", off)
}
on := runCheapGates("", draft, final, cheapGateConfig{yoPolicy: "auto", allowlist: map[string]bool{}, regressionEnabled: true})
if on.LengthCollapse == 0 {
t.Errorf("regression guard did not fire while enabled: %+v", on)
}
if on.total() <= off.total() {
t.Errorf("enabled guard must raise the observability total: off=%d on=%d", off.total(), on.total())
}
}
// TestRegressionGuardDeterministic — a pure function of (draft, final), resume-safe.
func TestRegressionGuardDeterministic(t *testing.T) {
d := strings.Repeat("текст с числом 44 и 128. ", 20)
f := "короткий финал без чисел"
a, b := runRegressionGuard(d, f), runRegressionGuard(d, f)
if a.LengthCollapse != b.LengthCollapse || a.NumberDrift != b.NumberDrift ||
strings.Join(a.Detail, "|") != strings.Join(b.Detail, "|") {
t.Fatalf("runRegressionGuard not deterministic: %+v vs %+v", a, b)
}
}